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/metrics_server/metrics_server_spec.rb36
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb4
-rw-r--r--spec/components/pajamas/empty_state_component_spec.rb101
-rw-r--r--spec/components/previews/pajamas/empty_state_component_preview.rb36
-rw-r--r--spec/config/settings_spec.rb4
-rw-r--r--spec/contracts/provider/spec_helper.rb2
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb37
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb26
-rw-r--r--spec/controllers/application_controller_spec.rb14
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb4
-rw-r--r--spec/controllers/concerns/kas_cookie_spec.rb62
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb195
-rw-r--r--spec/controllers/concerns/onboarding/status_spec.rb106
-rw-r--r--spec/controllers/dashboard_controller_spec.rb17
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb53
-rw-r--r--spec/controllers/graphql_controller_spec.rb33
-rw-r--r--spec/controllers/groups/children_controller_spec.rb6
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb106
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb20
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb11
-rw-r--r--spec/controllers/projects/alert_management_controller_spec.rb59
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb8
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb34
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb78
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb12
-rw-r--r--spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb2
-rw-r--r--spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb4
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb268
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb53
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb104
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb13
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb106
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb5
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb171
-rw-r--r--spec/controllers/projects_controller_spec.rb156
-rw-r--r--spec/controllers/registrations/welcome_controller_spec.rb2
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb10
-rw-r--r--spec/db/schema_spec.rb5
-rw-r--r--spec/deprecation_warnings.rb2
-rw-r--r--spec/experiments/application_experiment_spec.rb2
-rw-r--r--spec/experiments/concerns/project_commit_count_spec.rb41
-rw-r--r--spec/experiments/force_company_trial_experiment_spec.rb24
-rw-r--r--spec/factories/ai/service_access_tokens.rb21
-rw-r--r--spec/factories/alert_management/http_integrations.rb10
-rw-r--r--spec/factories/audit_events.rb23
-rw-r--r--spec/factories/boards.rb1
-rw-r--r--spec/factories/bulk_import/trackers.rb8
-rw-r--r--spec/factories/ci/external_pull_requests.rb (renamed from spec/factories/external_pull_requests.rb)2
-rw-r--r--spec/factories/ci/pipelines.rb4
-rw-r--r--spec/factories/events.rb2
-rw-r--r--spec/factories/integrations.rb13
-rw-r--r--spec/factories/issues.rb12
-rw-r--r--spec/factories/ml/model_versions.rb16
-rw-r--r--spec/factories/ml/models.rb10
-rw-r--r--spec/factories/organizations/organization_settings.rb7
-rw-r--r--spec/factories/organizations/organization_users.rb8
-rw-r--r--spec/factories/packages/packages.rb2
-rw-r--r--spec/factories/project_authorizations.rb4
-rw-r--r--spec/factories/project_hooks.rb1
-rw-r--r--spec/factories/work_items.rb11
-rw-r--r--spec/factories/work_items/work_item_types.rb5
-rw-r--r--spec/fast_spec_helper.rb4
-rw-r--r--spec/features/abuse_report_spec.rb5
-rw-r--r--spec/features/admin/admin_hooks_spec.rb7
-rw-r--r--spec/features/admin/admin_mode/login_spec.rb6
-rw-r--r--spec/features/admin/admin_runners_spec.rb27
-rw-r--r--spec/features/admin/admin_settings_spec.rb45
-rw-r--r--spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb2
-rw-r--r--spec/features/admin/users/user_spec.rb4
-rw-r--r--spec/features/atom/issues_spec.rb14
-rw-r--r--spec/features/atom/merge_requests_spec.rb14
-rw-r--r--spec/features/atom/topics_spec.rb49
-rw-r--r--spec/features/atom/users_spec.rb36
-rw-r--r--spec/features/boards/boards_spec.rb3
-rw-r--r--spec/features/boards/issue_ordering_spec.rb16
-rw-r--r--spec/features/boards/multi_select_spec.rb16
-rw-r--r--spec/features/boards/new_issue_spec.rb31
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb5
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb2
-rw-r--r--spec/features/boards/user_visits_board_spec.rb2
-rw-r--r--spec/features/clusters/create_agent_spec.rb3
-rw-r--r--spec/features/commits_spec.rb39
-rw-r--r--spec/features/dashboard/activity_spec.rb14
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb9
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb9
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb55
-rw-r--r--spec/features/dashboard/todos/todos_sorting_spec.rb5
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb25
-rw-r--r--spec/features/discussion_comments/issue_spec.rb3
-rw-r--r--spec/features/error_tracking/user_filters_errors_by_status_spec.rb2
-rw-r--r--spec/features/error_tracking/user_searches_sentry_errors_spec.rb2
-rw-r--r--spec/features/error_tracking/user_sees_error_details_spec.rb2
-rw-r--r--spec/features/error_tracking/user_sees_error_index_spec.rb2
-rw-r--r--spec/features/file_uploads/multipart_invalid_uploads_spec.rb2
-rw-r--r--spec/features/groups/board_spec.rb3
-rw-r--r--spec/features/groups/group_runners_spec.rb15
-rw-r--r--spec/features/groups/milestone_spec.rb8
-rw-r--r--spec/features/groups/milestones/gfm_autocomplete_spec.rb6
-rw-r--r--spec/features/groups/packages_spec.rb2
-rw-r--r--spec/features/groups/participants_autocomplete_spec.rb50
-rw-r--r--spec/features/groups/settings/access_tokens_spec.rb2
-rw-r--r--spec/features/groups_spec.rb8
-rw-r--r--spec/features/help_pages_spec.rb8
-rw-r--r--spec/features/ics/dashboard_issues_spec.rb67
-rw-r--r--spec/features/ics/group_issues_spec.rb11
-rw-r--r--spec/features/ics/project_issues_spec.rb11
-rw-r--r--spec/features/incidents/incident_timeline_events_spec.rb10
-rw-r--r--spec/features/incidents/user_views_incident_spec.rb10
-rw-r--r--spec/features/invites_spec.rb3
-rw-r--r--spec/features/issuables/issuable_list_spec.rb4
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb3
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb10
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb13
-rw-r--r--spec/features/issues/form_spec.rb4
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb13
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb16
-rw-r--r--spec/features/issues/note_polling_spec.rb6
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb3
-rw-r--r--spec/features/issues/related_issues_spec.rb4
-rw-r--r--spec/features/issues/service_desk_spec.rb67
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb9
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb20
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb4
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb8
-rw-r--r--spec/features/issues/user_filters_issues_spec.rb12
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb2
-rw-r--r--spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb5
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/labels_hierarchy_spec.rb4
-rw-r--r--spec/features/markdown/keyboard_shortcuts_spec.rb15
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb18
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb9
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb12
-rw-r--r--spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb25
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb12
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb17
-rw-r--r--spec/features/merge_request/user_edits_merge_request_spec.rb4
-rw-r--r--spec/features/merge_request/user_edits_mr_spec.rb6
-rw-r--r--spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb25
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb3
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb157
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb201
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb16
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb17
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb8
-rw-r--r--spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb22
-rw-r--r--spec/features/merge_request/user_resolves_wip_mr_spec.rb22
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb5
-rw-r--r--spec/features/merge_request/user_sees_discussions_navigation_spec.rb44
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb90
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb57
-rw-r--r--spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb11
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb20
-rw-r--r--spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb21
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb25
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb24
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb4
-rw-r--r--spec/features/merge_request/user_squashes_merge_request_spec.rb28
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb20
-rw-r--r--spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb24
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_open_merge_request_spec.rb3
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb87
-rw-r--r--spec/features/merge_requests/user_views_open_merge_requests_spec.rb10
-rw-r--r--spec/features/nav/top_nav_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb8
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb128
-rw-r--r--spec/features/projects/activity/user_sees_activity_spec.rb14
-rw-r--r--spec/features/projects/artifacts/user_downloads_artifacts_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb13
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb2
-rw-r--r--spec/features/projects/blobs/blame_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb2
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb24
-rw-r--r--spec/features/projects/branches_spec.rb23
-rw-r--r--spec/features/projects/ci/editor_spec.rb138
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb4
-rw-r--r--spec/features/projects/commit/builds_spec.rb4
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb12
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb10
-rw-r--r--spec/features/projects/compare_spec.rb12
-rw-r--r--spec/features/projects/environments/environment_spec.rb24
-rw-r--r--spec/features/projects/environments/environments_spec.rb68
-rw-r--r--spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb3
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb11
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb9
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb8
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_prometheus_spec.rb1
-rw-r--r--spec/features/projects/issuable_templates_spec.rb8
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_spec.rb71
-rw-r--r--spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb23
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb16
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb2
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb8
-rw-r--r--spec/features/projects/milestones/gfm_autocomplete_spec.rb6
-rw-r--r--spec/features/projects/navbar_spec.rb6
-rw-r--r--spec/features/projects/pages/user_adds_domain_spec.rb2
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb40
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb421
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb63
-rw-r--r--spec/features/projects/releases/user_views_release_spec.rb10
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb2
-rw-r--r--spec/features/projects/settings/external_authorization_service_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb7
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb2
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/service_desk_setting_spec.rb35
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb19
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb5
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb24
-rw-r--r--spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb4
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb22
-rw-r--r--spec/features/projects/user_sees_user_popover_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb21
-rw-r--r--spec/features/runners_spec.rb66
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb8
-rw-r--r--spec/features/snippets/search_snippets_spec.rb4
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb2
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb6
-rw-r--r--spec/features/task_lists_spec.rb9
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb2
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb7
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb12
-rw-r--r--spec/features/users/login_spec.rb17
-rw-r--r--spec/features/users/overview_spec.rb14
-rw-r--r--spec/features/users/rss_spec.rb8
-rw-r--r--spec/features/users/show_spec.rb28
-rw-r--r--spec/features/users/terms_spec.rb15
-rw-r--r--spec/finders/award_emojis_finder_spec.rb4
-rw-r--r--spec/finders/ci/group_variables_finder_spec.rb73
-rw-r--r--spec/finders/ci/runners_finder_spec.rb4
-rw-r--r--spec/finders/clusters/agent_tokens_finder_spec.rb5
-rw-r--r--spec/finders/deployments_finder_spec.rb39
-rw-r--r--spec/finders/group_descendants_finder_spec.rb8
-rw-r--r--spec/finders/group_projects_finder_spec.rb10
-rw-r--r--spec/finders/packages/ml_model/package_finder_spec.rb57
-rw-r--r--spec/finders/packages/npm/package_finder_spec.rb67
-rw-r--r--spec/finders/projects/ml/model_finder_spec.rb40
-rw-r--r--spec/finders/projects_finder_spec.rb30
-rw-r--r--spec/finders/users_finder_spec.rb13
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project_hook.json116
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/wiki_blobs.json57
-rw-r--r--spec/fixtures/api/schemas/slack/manifest.json1250
-rw-r--r--spec/fixtures/csv_missing_milestones.csv5
-rw-r--r--spec/fixtures/grafana/expected_grafana_embed.json6
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson6
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json60
-rw-r--r--spec/frontend/__helpers__/set_vue_error_handler.js30
-rw-r--r--spec/frontend/access_tokens/components/tokens_app_spec.js4
-rw-r--r--spec/frontend/actioncable_connection_monitor_spec.js79
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js17
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_category_spec.js43
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js16
-rw-r--r--spec/frontend/admin/applications/components/delete_application_spec.js7
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js43
-rw-r--r--spec/frontend/admin/topics/components/remove_avatar_spec.js2
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js4
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js31
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js13
-rw-r--r--spec/frontend/analytics/usage_trends/components/users_chart_spec.js47
-rw-r--r--spec/frontend/api/user_api_spec.js21
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js68
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js17
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js55
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js196
-rw-r--r--spec/frontend/behaviors/markdown/render_gfm_spec.js26
-rw-r--r--spec/frontend/behaviors/markdown/render_metrics_spec.js49
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js71
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js1
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js2
-rw-r--r--spec/frontend/boards/components/board_app_spec.js11
-rw-r--r--spec/frontend/boards/components/board_content_spec.js11
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js23
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js65
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js4
-rw-r--r--spec/frontend/boards/mock_data.js41
-rw-r--r--spec/frontend/boards/project_select_spec.js111
-rw-r--r--spec/frontend/boards/stores/actions_spec.js8
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap5
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js2
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js65
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js43
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js2
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js33
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js21
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js18
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js306
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js37
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js34
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap26
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js83
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js63
-rw-r--r--spec/frontend/ci/reports/mock_data/mock_data.js54
-rw-r--r--spec/frontend/ci/reports/utils_spec.js30
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js82
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js94
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js75
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js23
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_action_spec.js223
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js222
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js68
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_modal_spec.js34
-rw-r--r--spec/frontend/ci/runner/components/runner_detail_spec.js88
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js30
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js42
-rw-r--r--spec/frontend/ci/runner/components/runner_header_actions_spec.js147
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js159
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_action_spec.js180
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js282
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js71
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js67
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js29
-rw-r--r--spec/frontend/clusters/agents/components/create_token_modal_spec.js13
-rw-r--r--spec/frontend/clusters/agents/components/revoke_token_button_spec.js8
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js38
-rw-r--r--spec/frontend/clusters_list/components/delete_agent_button_spec.js10
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap2
-rw-r--r--spec/frontend/commit/commit_pipeline_status_spec.js (renamed from spec/frontend/commit/commit_pipeline_status_component_spec.js)2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js71
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js20
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js104
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js9
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js239
-rw-r--r--spec/frontend/content_editor/components/wrappers/image_spec.js100
-rw-r--r--spec/frontend/content_editor/components/wrappers/reference_spec.js18
-rw-r--r--spec/frontend/content_editor/extensions/code_suggestion_spec.js128
-rw-r--r--spec/frontend/content_editor/extensions/comment_spec.js30
-rw-r--r--spec/frontend/content_editor/extensions/copy_paste_spec.js (renamed from spec/frontend/content_editor/extensions/paste_markdown_spec.js)110
-rw-r--r--spec/frontend/content_editor/extensions/hard_break_spec.js20
-rw-r--r--spec/frontend/content_editor/extensions/html_nodes_spec.js6
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/paragraph_spec.js29
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js13
-rw-r--r--spec/frontend/content_editor/services/code_suggestion_utils_spec.js53
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js8
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js13
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js27
-rw-r--r--spec/frontend/content_editor/test_utils.js19
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js34
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js79
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js30
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js30
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js30
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js31
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js33
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js141
-rw-r--r--spec/frontend/contribution_events/components/contribution_events_spec.js37
-rw-r--r--spec/frontend/contribution_events/components/resource_parent_link_spec.js46
-rw-r--r--spec/frontend/contribution_events/components/target_link_spec.js43
-rw-r--r--spec/frontend/contribution_events/utils.js52
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js82
-rw-r--r--spec/frontend/design_management/components/design_description/description_form_spec.js35
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap86
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js12
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js268
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js4
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js29
-rw-r--r--spec/frontend/design_management/mock_data/discussion.js12
-rw-r--r--spec/frontend/design_management/mock_data/notes.js3
-rw-r--r--spec/frontend/diffs/components/app_spec.js12
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_item_spec.js37
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js55
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js72
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js16
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js26
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js181
-rw-r--r--spec/frontend/diffs/components/diff_inline_findings_spec.js33
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js40
-rw-r--r--spec/frontend/diffs/components/diff_line_spec.js21
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js14
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js36
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js87
-rw-r--r--spec/frontend/diffs/store/actions_spec.js65
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js2
-rw-r--r--spec/frontend/diffs/store/utils_spec.js6
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js1
-rw-r--r--spec/frontend/dropzone_input_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js1
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js7
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js4
-rw-r--r--spec/frontend/emoji/index_spec.js95
-rw-r--r--spec/frontend/environments/edit_environment_spec.js158
-rw-r--r--spec/frontend/environments/environment_form_spec.js247
-rw-r--r--spec/frontend/environments/graphql/mock_data.js5
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js46
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js60
-rw-r--r--spec/frontend/environments/new_environment_spec.js108
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js11
-rw-r--r--spec/frontend/fixtures/groups.rb33
-rw-r--r--spec/frontend/fixtures/issues.rb4
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb42
-rw-r--r--spec/frontend/fixtures/milestones.rb43
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb6
-rw-r--r--spec/frontend/fixtures/static/line_highlighter.html85
-rw-r--r--spec/frontend/fixtures/static/textarea.html27
-rw-r--r--spec/frontend/fixtures/timezones.rb2
-rw-r--r--spec/frontend/fixtures/users.rb9
-rw-r--r--spec/frontend/frequent_items/mock_data.js2
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js2
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js33
-rw-r--r--spec/frontend/groups/components/app_spec.js8
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js67
-rw-r--r--spec/frontend/groups/service/archived_projects_service_spec.js90
-rw-r--r--spec/frontend/groups/service/groups_service_spec.js19
-rw-r--r--spec/frontend/header_search/init_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js44
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js1
-rw-r--r--spec/frontend/ide/mock_data.js2
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js174
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js31
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js13
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js6
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js2
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js35
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js2
-rw-r--r--spec/frontend/issuable/popover/components/mr_popover_spec.js2
-rw-r--r--spec/frontend/issuable/popover/index_spec.js68
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js51
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js109
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js3
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js1
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js1
-rw-r--r--spec/frontend/issues/show/components/delete_issue_modal_spec.js9
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js103
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js36
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js10
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js6
-rw-r--r--spec/frontend/issues/show/issue_spec.js2
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js1
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js1
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js8
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js45
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js2
-rw-r--r--spec/frontend/jobs/components/job/job_container_item_spec.js16
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js34
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js15
-rw-r--r--spec/frontend/lib/utils/downloader_spec.js4
-rw-r--r--spec/frontend/lib/utils/forms_spec.js111
-rw-r--r--spec/frontend/lib/utils/ref_validator_spec.js23
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js2
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js10
-rw-r--r--spec/frontend/merge_requests/generated_content_spec.js310
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js39
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js12
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap155
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap55
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap160
-rw-r--r--spec/frontend/monitoring/components/charts/annotations_spec.js95
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js304
-rw-r--r--spec/frontend/monitoring/components/charts/bar_spec.js53
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js118
-rw-r--r--spec/frontend/monitoring/components/charts/empty_chart_spec.js21
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js210
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js93
-rw-r--r--spec/frontend/monitoring/components/charts/options_spec.js327
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js94
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js193
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js748
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js44
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js421
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js395
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js226
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js582
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js784
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js41
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js159
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js170
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js166
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js110
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js157
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js100
-rw-r--r--spec/frontend/monitoring/components/embeds/mock_data.js86
-rw-r--r--spec/frontend/monitoring/components/empty_state_spec.js55
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js144
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js47
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js64
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js139
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js62
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js55
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js125
-rw-r--r--spec/frontend/monitoring/csv_export_spec.js126
-rw-r--r--spec/frontend/monitoring/fixture_data.js49
-rw-r--r--spec/frontend/monitoring/graph_data.js274
-rw-r--r--spec/frontend/monitoring/mock_data.js574
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js60
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js93
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js157
-rw-r--r--spec/frontend/monitoring/router_spec.js106
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js1167
-rw-r--r--spec/frontend/monitoring/store/embed_group/actions_spec.js16
-rw-r--r--spec/frontend/monitoring/store/embed_group/getters_spec.js19
-rw-r--r--spec/frontend/monitoring/store/embed_group/mutations_spec.js16
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js457
-rw-r--r--spec/frontend/monitoring/store/index_spec.js23
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js586
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js893
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js209
-rw-r--r--spec/frontend/monitoring/store_utils.js80
-rw-r--r--spec/frontend/monitoring/stubs/modal_stub.js11
-rw-r--r--spec/frontend/monitoring/utils_spec.js464
-rw-r--r--spec/frontend/monitoring/validators_spec.js80
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js51
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js40
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js105
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js25
-rw-r--r--spec/frontend/notes/components/mr_discussion_filter_spec.js28
-rw-r--r--spec/frontend/notes/components/note_form_spec.js35
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js30
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js26
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js3
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js1
-rw-r--r--spec/frontend/notes/mock_data.js2
-rw-r--r--spec/frontend/notes/utils_spec.js31
-rw-r--r--spec/frontend/notifications/components/notification_email_listbox_input_spec.js2
-rw-r--r--spec/frontend/observability/client_spec.js66
-rw-r--r--spec/frontend/observability/observability_app_spec.js15
-rw-r--r--spec/frontend/observability/observability_container_spec.js134
-rw-r--r--spec/frontend/observability/skeleton_spec.js44
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/app_spec.js99
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/mock_data.js98
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js36
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js94
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js81
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js143
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js40
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap21
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js82
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js26
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js6
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js6
-rw-r--r--spec/frontend/pages/groups/new/components/app_spec.js5
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js13
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js45
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js46
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js252
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js236
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js54
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js114
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js140
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js56
-rw-r--r--spec/frontend/pipelines/graph/job_name_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js9
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js1
-rw-r--r--spec/frontend/pipelines/header_component_spec.js246
-rw-r--r--spec/frontend/pipelines/mock_data.js8
-rw-r--r--spec/frontend/pipelines/pipeline_details_header_spec.js44
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js23
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js26
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js13
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js7
-rw-r--r--spec/frontend/profile/components/follow_spec.js49
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js2
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js108
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js19
-rw-r--r--spec/frontend/profile/components/snippets/snippets_tab_spec.js43
-rw-r--r--spec/frontend/profile/mock_data.js1
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js8
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js110
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js104
-rw-r--r--spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap30
-rw-r--r--spec/frontend/projects/new/components/app_spec.js6
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js25
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js1
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js10
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js24
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js3
-rw-r--r--spec/frontend/related_issues/components/related_issuable_input_spec.js98
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js15
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap4
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js7
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js43
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js8
-rw-r--r--spec/frontend/scripts/frontend/po_to_json_spec.js8
-rw-r--r--spec/frontend/search/mock_data.js21
-rw-r--r--spec/frontend/search/sidebar/components/label_filter_spec.js30
-rw-r--r--spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js6
-rw-r--r--spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js16
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js5
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js5
-rw-r--r--spec/frontend/service_desk/components/info_banner_spec.js81
-rw-r--r--spec/frontend/service_desk/components/service_desk_list_app_spec.js151
-rw-r--r--spec/frontend/service_desk/mock_data.js118
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js96
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_title_spec.js21
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js22
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js8
-rw-r--r--spec/frontend/sidebar/components/participants/participants_spec.js13
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js66
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js11
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js50
-rw-r--r--spec/frontend/sidebar/components/time_tracking/mock_data.js13
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js57
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap1
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js1
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap9
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js11
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js93
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js43
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js175
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js14
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js6
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js12
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js8
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js2
-rw-r--r--spec/frontend/super_sidebar/mock_data.js1
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js32
-rw-r--r--spec/frontend/tags/components/delete_tag_modal_spec.js19
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js125
-rw-r--r--spec/frontend/tracing/components/tracing_empty_state_spec.js44
-rw-r--r--spec/frontend/tracing/components/tracing_list_spec.js131
-rw-r--r--spec/frontend/tracing/components/tracing_table_list_spec.js63
-rw-r--r--spec/frontend/tracing/list_index_spec.js37
-rw-r--r--spec/frontend/tracking/internal_events_spec.js100
-rw-r--r--spec/frontend/tracking/tracking_spec.js1
-rw-r--r--spec/frontend/tracking/utils_spec.js37
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js9
-rw-r--r--spec/frontend/usage_quotas/storage/mock_data.js12
-rw-r--r--spec/frontend/users/profile/actions/components/user_actions_app_spec.js38
-rw-r--r--spec/frontend/vue_compat_test_setup.js74
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js25
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js51
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap32
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js46
-rw-r--r--spec/frontend/vue_merge_request_widget/mock_data.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js208
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js65
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js120
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/entity_select/group_select_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js47
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js100
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js112
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js157
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js73
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap144
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/security_reports/help_icon_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/security_reports/security_summary_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js27
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js25
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/welcome_spec.js12
-rw-r--r--spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js9
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js136
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js267
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js182
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js197
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js84
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js198
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js84
-rw-r--r--spec/frontend/vue_shared/security_reports/store/utils_spec.js63
-rw-r--r--spec/frontend/vue_shared/security_reports/utils_spec.js48
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js63
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js147
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js30
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js107
-rw-r--r--spec/frontend/work_items/components/work_item_award_emoji_spec.js231
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js13
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js292
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_todos_spec.js111
-rw-r--r--spec/frontend/work_items/mock_data.js258
-rw-r--r--spec/frontend/work_items/notes/award_utils_spec.js109
-rw-r--r--spec/frontend/work_items/router_spec.js39
-rw-r--r--spec/frontend/work_items/utils_spec.js21
-rw-r--r--spec/frontend_integration/content_editor/content_editor_integration_spec.js9
-rw-r--r--spec/graphql/gitlab_schema_spec.rb6
-rw-r--r--spec/graphql/graphql_triggers_spec.rb22
-rw-r--r--spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb13
-rw-r--r--spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb40
-rw-r--r--spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb2
-rw-r--r--spec/graphql/mutations/issues/create_spec.rb6
-rw-r--r--spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb3
-rw-r--r--spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb3
-rw-r--r--spec/graphql/resolvers/board_lists_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/config_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/inherited_variables_resolver_spec.rb53
-rw-r--r--spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_job_count_resolver_spec.rb56
-rw-r--r--spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb17
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/design_management/version_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/echo_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/issue_status_counts_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb56
-rw-r--r--spec/graphql/resolvers/project_issues_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/users/participants_resolver_spec.rb3
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb1
-rw-r--r--spec/graphql/types/ci/detailed_status_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/group_variable_type_spec.rb4
-rw-r--r--spec/graphql/types/ci/project_variable_type_spec.rb4
-rw-r--r--spec/graphql/types/commit_signatures/verification_status_enum_spec.rb2
-rw-r--r--spec/graphql/types/deployment_tag_type_spec.rb (renamed from spec/graphql/types/detployment_tag_type_spec.rb)2
-rw-r--r--spec/graphql/types/environment_type_spec.rb2
-rw-r--r--spec/graphql/types/global_id_type_spec.rb6
-rw-r--r--spec/graphql/types/ide_type_spec.rb15
-rw-r--r--spec/graphql/types/issue_type_spec.rb1
-rw-r--r--spec/graphql/types/metrics/dashboard_type_spec.rb22
-rw-r--r--spec/graphql/types/project_statistics_type_spec.rb4
-rw-r--r--spec/graphql/types/project_type_spec.rb8
-rw-r--r--spec/graphql/types/root_storage_statistics_type_spec.rb6
-rw-r--r--spec/graphql/types/user_type_spec.rb42
-rw-r--r--spec/helpers/admin/application_settings/settings_helper_spec.rb6
-rw-r--r--spec/helpers/application_helper_spec.rb86
-rw-r--r--spec/helpers/button_helper_spec.rb119
-rw-r--r--spec/helpers/calendar_helper_spec.rb5
-rw-r--r--spec/helpers/ci/jobs_helper_spec.rb7
-rw-r--r--spec/helpers/ci/pipeline_schedules_helper_spec.rb31
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb22
-rw-r--r--spec/helpers/clusters_helper_spec.rb15
-rw-r--r--spec/helpers/environment_helper_spec.rb12
-rw-r--r--spec/helpers/environments_helper_spec.rb35
-rw-r--r--spec/helpers/feed_token_helper_spec.rb28
-rw-r--r--spec/helpers/groups_helper_spec.rb15
-rw-r--r--spec/helpers/integrations_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb6
-rw-r--r--spec/helpers/issues_helper_spec.rb3
-rw-r--r--spec/helpers/markup_helper_spec.rb2
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb10
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb10
-rw-r--r--spec/helpers/packages_helper_spec.rb111
-rw-r--r--spec/helpers/page_layout_helper_spec.rb2
-rw-r--r--spec/helpers/projects/observability_helper_spec.rb21
-rw-r--r--spec/helpers/projects/pipeline_helper_spec.rb2
-rw-r--r--spec/helpers/projects_helper_spec.rb154
-rw-r--r--spec/helpers/rss_helper_spec.rb5
-rw-r--r--spec/helpers/search_helper_spec.rb378
-rw-r--r--spec/helpers/sidebars_helper_spec.rb41
-rw-r--r--spec/helpers/tree_helper_spec.rb7
-rw-r--r--spec/helpers/users_helper_spec.rb18
-rw-r--r--spec/helpers/web_hooks/web_hooks_helper_spec.rb4
-rw-r--r--spec/initializers/00_rails_disable_joins_spec.rb288
-rw-r--r--spec/initializers/100_patch_omniauth_saml_spec.rb9
-rw-r--r--spec/initializers/action_dispatch_journey_router_spec.rb36
-rw-r--r--spec/initializers/google_api_client_spec.rb2
-rw-r--r--spec/initializers/grpc_patch_spec.rb31
-rw-r--r--spec/initializers/secret_token_spec.rb4
-rw-r--r--spec/lib/api/entities/bulk_imports/export_batch_status_spec.rb21
-rw-r--r--spec/lib/api/entities/bulk_imports/export_status_spec.rb19
-rw-r--r--spec/lib/api/entities/plan_limit_spec.rb1
-rw-r--r--spec/lib/api/entities/project_spec.rb2
-rw-r--r--spec/lib/api/helpers/packages/npm_spec.rb26
-rw-r--r--spec/lib/api/helpers_spec.rb39
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb20
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb168
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb20
-rw-r--r--spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb32
-rw-r--r--spec/lib/banzai/filter/references/label_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb14
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb2
-rw-r--r--spec/lib/container_registry/client_spec.rb4
-rw-r--r--spec/lib/container_registry/gitlab_api_client_spec.rb6
-rw-r--r--spec/lib/expand_variables_spec.rb102
-rw-r--r--spec/lib/extracts_ref/requested_ref_spec.rb153
-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/ee_my_batched_migration.txt29
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/foss_my_batched_migration.txt14
-rw-r--r--spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb24
-rw-r--r--spec/lib/gitlab/access/branch_protection_spec.rb58
-rw-r--r--spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb16
-rw-r--r--spec/lib/gitlab/alert_management/payload/prometheus_spec.rb28
-rw-r--r--spec/lib/gitlab/asciidoc/html5_converter_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb75
-rw-r--r--spec/lib/gitlab/auth_spec.rb45
-rw-r--r--spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb98
-rw-r--r--spec/lib/gitlab/background_migration/backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb133
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb37
-rw-r--r--spec/lib/gitlab/buffered_io_spec.rb4
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb16
-rw-r--r--spec/lib/gitlab/cache/client_spec.rb60
-rw-r--r--spec/lib/gitlab/cache/metadata_spec.rb13
-rw-r--r--spec/lib/gitlab/cache/metrics_spec.rb90
-rw-r--r--spec/lib/gitlab/checks/changes_access_spec.rb8
-rw-r--r--spec/lib/gitlab/checks/diff_check_spec.rb4
-rw-r--r--spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb86
-rw-r--r--spec/lib/gitlab/checks/file_size_check/any_oversized_blobs_spec.rb31
-rw-r--r--spec/lib/gitlab/checks/global_file_size_check_spec.rb36
-rw-r--r--spec/lib/gitlab/checks/snippet_check_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/artifact_file_reader_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/config/external/file/artifact_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/external/file/component_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/filter_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.rb40
-rw-r--r--spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb144
-rw-r--r--spec/lib/gitlab/ci/config/yaml/loader_spec.rb165
-rw-r--r--spec/lib/gitlab/ci/config/yaml_spec.rb172
-rw-r--r--spec/lib/gitlab/ci/jwt_v2_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/source_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/stage/play_manual_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/tags/bulk_insert_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/sort_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/variables/downstream/generator_spec.rb85
-rw-r--r--spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb58
-rw-r--r--spec/lib/gitlab/cleanup/remote_uploads_spec.rb26
-rw-r--r--spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/composable_array_spec.rb4
-rw-r--r--spec/lib/gitlab/config/entry/composable_hash_spec.rb6
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb19
-rw-r--r--spec/lib/gitlab/data_builder/emoji_spec.rb126
-rw-r--r--spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb15
-rw-r--r--spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb15
-rw-r--r--spec/lib/gitlab/database/click_house_client_spec.rb113
-rw-r--r--spec/lib/gitlab/database/each_database_spec.rb14
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb3
-rw-r--r--spec/lib/gitlab/database/health_status_spec.rb4
-rw-r--r--spec/lib/gitlab/database/load_balancing/host_spec.rb2
-rw-r--r--spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb27
-rw-r--r--spec/lib/gitlab/database/load_balancing/primary_host_spec.rb2
-rw-r--r--spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb82
-rw-r--r--spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb37
-rw-r--r--spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/redis_helpers_spec.rb33
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb19
-rw-r--r--spec/lib/gitlab/database/partitioning_spec.rb2
-rw-r--r--spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb43
-rw-r--r--spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb6
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb18
-rw-r--r--spec/lib/gitlab/database/schema_validation/adapters/column_database_adapter_spec.rb72
-rw-r--r--spec/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter_spec.rb78
-rw-r--r--spec/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter_spec.rb28
-rw-r--r--spec/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter_spec.rb42
-rw-r--r--spec/lib/gitlab/database/schema_validation/database_spec.rb93
-rw-r--r--spec/lib/gitlab/database/schema_validation/inconsistency_spec.rb96
-rw-r--r--spec/lib/gitlab/database/schema_validation/runner_spec.rb50
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/column_spec.rb25
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/foreign_key_spec.rb25
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb11
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/table_spec.rb45
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb11
-rw-r--r--spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb66
-rw-r--r--spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb185
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb39
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys_spec.rb8
-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_tables_spec.rb7
-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_foreign_keys_spec.rb7
-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_table_columns_spec.rb7
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/extra_tables_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_foreign_keys_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_table_columns_spec.rb7
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/missing_tables_spec.rb9
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb9
-rw-r--r--spec/lib/gitlab/database/similarity_score_spec.rb10
-rw-r--r--spec/lib/gitlab/database_spec.rb12
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb2
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb119
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb2
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb19
-rw-r--r--spec/lib/gitlab/encrypted_configuration_spec.rb4
-rw-r--r--spec/lib/gitlab/error_tracking/logger_spec.rb2
-rw-r--r--spec/lib/gitlab/error_tracking/processor/sanitize_error_message_processor_spec.rb4
-rw-r--r--spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb2
-rw-r--r--spec/lib/gitlab/exception_log_formatter_spec.rb48
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb10
-rw-r--r--spec/lib/gitlab/git/base_error_spec.rb29
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb68
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb5
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb18
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb16
-rw-r--r--spec/lib/gitlab/git/finders/refs_finder_spec.rb62
-rw-r--r--spec/lib/gitlab/git/keep_around_spec.rb15
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb56
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb9
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/call_spec.rb47
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb140
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb3
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/client_pool_spec.rb41
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb58
-rw-r--r--spec/lib/gitlab/github_import/settings_spec.rb21
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb75
-rw-r--r--spec/lib/gitlab/github_import_spec.rb25
-rw-r--r--spec/lib/gitlab/gl_repository/repo_type_spec.rb8
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb49
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb16
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb8
-rw-r--r--spec/lib/gitlab/graphql/generic_tracing_spec.rb89
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb2
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/hook_data/emoji_builder_spec.rb27
-rw-r--r--spec/lib/gitlab/i18n/pluralization_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml10
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/project/object_builder_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/project/relation_factory_spec.rb32
-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.rb8
-rw-r--r--spec/lib/gitlab/import_export/references_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml5
-rw-r--r--spec/lib/gitlab/import_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb36
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb47
-rw-r--r--spec/lib/gitlab/instrumentation/redis_spec.rb22
-rw-r--r--spec/lib/gitlab/internal_events/event_definitions_spec.rb102
-rw-r--r--spec/lib/gitlab/internal_events_spec.rb79
-rw-r--r--spec/lib/gitlab/issues/rebalancing/state_spec.rb3
-rw-r--r--spec/lib/gitlab/jwt_authenticatable_spec.rb26
-rw-r--r--spec/lib/gitlab/kas/client_spec.rb26
-rw-r--r--spec/lib/gitlab/kas/user_access_spec.rb36
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb2
-rw-r--r--spec/lib/gitlab/lograge/custom_options_spec.rb10
-rw-r--r--spec/lib/gitlab/manifest_import/metadata_spec.rb39
-rw-r--r--spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb2
-rw-r--r--spec/lib/gitlab/markdown_cache/redis/extension_spec.rb4
-rw-r--r--spec/lib/gitlab/memory/reporter_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/dashboard/url_spec.rb107
-rw-r--r--spec/lib/gitlab/metrics/environment_spec.rb3
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_slis_spec.rb58
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/multi_destination_logger_spec.rb3
-rw-r--r--spec/lib/gitlab/observability_spec.rb42
-rw-r--r--spec/lib/gitlab/pages/url_builder_spec.rb227
-rw-r--r--spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb2
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb63
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb32
-rw-r--r--spec/lib/gitlab/pagination/keyset/iterator_spec.rb2
-rw-r--r--spec/lib/gitlab/pagination/keyset/order_spec.rb54
-rw-r--r--spec/lib/gitlab/pagination/offset_pagination_spec.rb24
-rw-r--r--spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb28
-rw-r--r--spec/lib/gitlab/prometheus/query_variables_spec.rb2
-rw-r--r--spec/lib/gitlab/rack_attack/request_spec.rb5
-rw-r--r--spec/lib/gitlab/redis/cross_slot_spec.rb27
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb118
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/relative_positioning/range_spec.rb6
-rw-r--r--spec/lib/gitlab/request_forgery_protection_spec.rb5
-rw-r--r--spec/lib/gitlab/runtime_spec.rb10
-rw-r--r--spec/lib/gitlab/search/found_wiki_page_spec.rb2
-rw-r--r--spec/lib/gitlab/search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/seeder_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb40
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/defer_jobs_spec.rb111
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb26
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb100
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/size_limiter/client_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb151
-rw-r--r--spec/lib/gitlab/sidekiq_middleware_spec.rb12
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/access_spec.rb2
-rw-r--r--spec/lib/gitlab/spamcheck/client_spec.rb4
-rw-r--r--spec/lib/gitlab/ssh/commit_spec.rb5
-rw-r--r--spec/lib/gitlab/ssh/signature_spec.rb11
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb106
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb5
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migrations_metric_spec.rb27
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb32
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb50
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_gbp_metric_spec.rb16
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_metric_spec.rb13
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/gitaly_apdex_metric_spec.rb25
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb6
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/ldap_encrypted_secrets_metric_spec.rb23
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/operating_system_metric_spec.rb23
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/schema_inconsistencies_metric_spec.rb43
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/smtp_encrypted_secrets_metric_spec.rb23
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb65
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb9
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb39
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb6
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb106
-rw-r--r--spec/lib/gitlab/utils/measuring_spec.rb2
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb374
-rw-r--r--spec/lib/gitlab/utils_spec.rb477
-rw-r--r--spec/lib/gitlab/version_info_spec.rb193
-rw-r--r--spec/lib/gitlab/webpack/file_loader_spec.rb12
-rw-r--r--spec/lib/gitlab/webpack/manifest_spec.rb20
-rw-r--r--spec/lib/gitlab/x509/commit_spec.rb2
-rw-r--r--spec/lib/gitlab/x509/signature_spec.rb16
-rw-r--r--spec/lib/gitlab_settings/options_spec.rb118
-rw-r--r--spec/lib/result_spec.rb328
-rw-r--r--spec/lib/search/navigation_spec.rb280
-rw-r--r--spec/lib/service_ping/devops_report_spec.rb2
-rw-r--r--spec/lib/sidebars/groups/super_sidebar_menus/analyze_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/organizations/menus/manage_menu_spec.rb28
-rw-r--r--spec/lib/sidebars/organizations/menus/scope_menu_spec.rb21
-rw-r--r--spec/lib/sidebars/organizations/panel_spec.rb17
-rw-r--r--spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb39
-rw-r--r--spec/lib/sidebars/panel_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/deployments_menu_spec.rb26
-rw-r--r--spec/lib/sidebars/projects/menus/monitor_menu_spec.rb18
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb30
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb1
-rw-r--r--spec/lib/slack/manifest_spec.rb99
-rw-r--r--spec/mailers/emails/in_product_marketing_spec.rb2
-rw-r--r--spec/mailers/emails/issues_spec.rb9
-rw-r--r--spec/mailers/emails/projects_spec.rb3
-rw-r--r--spec/mailers/emails/releases_spec.rb2
-rw-r--r--spec/mailers/emails/service_desk_spec.rb24
-rw-r--r--spec/mailers/notify_spec.rb49
-rw-r--r--spec/metrics_server/metrics_server_spec.rb179
-rw-r--r--spec/migrations/20230523101514_finalize_user_type_migration_spec.rb9
-rw-r--r--spec/migrations/20230530012406_finalize_backfill_resource_link_events_spec.rb68
-rw-r--r--spec/migrations/20230613192703_ensure_ci_build_needs_big_int_backfill_is_finished_for_self_hosts_spec.rb25
-rw-r--r--spec/migrations/20230613192703_swap_ci_build_needs_to_big_int_for_self_hosts_spec.rb146
-rw-r--r--spec/migrations/20230616082958_add_unique_index_for_npm_packages_on_project_id_name_version_spec.rb20
-rw-r--r--spec/migrations/20230621070810_update_requeue_workers_in_application_settings_for_gitlab_com_spec.rb44
-rw-r--r--spec/migrations/20230621074611_update_elasticsearch_number_of_shards_in_application_settings_for_gitlab_com_spec.rb44
-rw-r--r--spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb26
-rw-r--r--spec/migrations/20230629095819_queue_backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb26
-rw-r--r--spec/migrations/20230703024031_cleanup_project_pipeline_status_key_spec.rb12
-rw-r--r--spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb107
-rw-r--r--spec/migrations/deduplicate_inactive_alert_integrations_spec.rb71
-rw-r--r--spec/migrations/ensure_events_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb35
-rw-r--r--spec/models/abuse/trust_score_spec.rb12
-rw-r--r--spec/models/abuse/user_trust_score_spec.rb130
-rw-r--r--spec/models/active_session_spec.rb56
-rw-r--r--spec/models/ai/service_access_token_spec.rb45
-rw-r--r--spec/models/alert_management/alert_spec.rb14
-rw-r--r--spec/models/alert_management/http_integration_spec.rb27
-rw-r--r--spec/models/analytics/cycle_analytics/stage_spec.rb15
-rw-r--r--spec/models/analytics/cycle_analytics/value_stream_spec.rb13
-rw-r--r--spec/models/application_record_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb54
-rw-r--r--spec/models/award_emoji_spec.rb25
-rw-r--r--spec/models/bulk_import_spec.rb18
-rw-r--r--spec/models/bulk_imports/entity_spec.rb42
-rw-r--r--spec/models/bulk_imports/export_spec.rb25
-rw-r--r--spec/models/bulk_imports/export_status_spec.rb96
-rw-r--r--spec/models/bulk_imports/file_transfer/group_config_spec.rb7
-rw-r--r--spec/models/ci/artifact_blob_spec.rb72
-rw-r--r--spec/models/ci/bridge_spec.rb85
-rw-r--r--spec/models/ci/build_dependencies_spec.rb2
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_report_result_spec.rb2
-rw-r--r--spec/models/ci/build_runner_session_spec.rb4
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/catalog/resource_spec.rb8
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb2
-rw-r--r--spec/models/ci/external_pull_request_spec.rb (renamed from spec/models/external_pull_request_spec.rb)8
-rw-r--r--spec/models/ci/group_variable_spec.rb53
-rw-r--r--spec/models/ci/job_artifact_spec.rb2
-rw-r--r--spec/models/ci/persistent_ref_spec.rb20
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb29
-rw-r--r--spec/models/ci/processable_spec.rb4
-rw-r--r--spec/models/ci/ref_spec.rb4
-rw-r--r--spec/models/ci/runner_manager_spec.rb43
-rw-r--r--spec/models/ci/runner_spec.rb3
-rw-r--r--spec/models/ci/stage_spec.rb2
-rw-r--r--spec/models/ci/variable_spec.rb5
-rw-r--r--spec/models/ci_platform_metric_spec.rb2
-rw-r--r--spec/models/clusters/agent_spec.rb32
-rw-r--r--spec/models/clusters/cluster_spec.rb2
-rw-r--r--spec/models/commit_spec.rb2
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/concerns/batch_destroy_dependent_associations_spec.rb2
-rw-r--r--spec/models/concerns/counter_attribute_spec.rb2
-rw-r--r--spec/models/concerns/database_event_tracking_spec.rb14
-rw-r--r--spec/models/concerns/expirable_spec.rb2
-rw-r--r--spec/models/concerns/group_descendant_spec.rb7
-rw-r--r--spec/models/concerns/has_user_type_spec.rb2
-rw-r--r--spec/models/concerns/integrations/enable_ssl_verification_spec.rb2
-rw-r--r--spec/models/concerns/integrations/reset_secret_fields_spec.rb2
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/concerns/milestoneish_spec.rb16
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb8
-rw-r--r--spec/models/concerns/spammable_spec.rb26
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb8
-rw-r--r--spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb2
-rw-r--r--spec/models/container_expiration_policy_spec.rb3
-rw-r--r--spec/models/container_repository_spec.rb6
-rw-r--r--spec/models/customer_relations/contact_spec.rb2
-rw-r--r--spec/models/customer_relations/organization_spec.rb2
-rw-r--r--spec/models/dependency_proxy/image_ttl_group_policy_spec.rb3
-rw-r--r--spec/models/dependency_proxy/manifest_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb10
-rw-r--r--spec/models/environment_spec.rb11
-rw-r--r--spec/models/group_spec.rb294
-rw-r--r--spec/models/import_failure_spec.rb6
-rw-r--r--spec/models/incident_management/timeline_event_spec.rb2
-rw-r--r--spec/models/integration_spec.rb44
-rw-r--r--spec/models/integrations/apple_app_store_spec.rb3
-rw-r--r--spec/models/integrations/asana_spec.rb14
-rw-r--r--spec/models/integrations/assembla_spec.rb39
-rw-r--r--spec/models/integrations/bamboo_spec.rb19
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb8
-rw-r--r--spec/models/integrations/base_issue_tracker_spec.rb6
-rw-r--r--spec/models/integrations/base_slack_notification_spec.rb2
-rw-r--r--spec/models/integrations/base_third_party_wiki_spec.rb6
-rw-r--r--spec/models/integrations/bugzilla_spec.rb2
-rw-r--r--spec/models/integrations/buildkite_spec.rb10
-rw-r--r--spec/models/integrations/chat_message/group_mention_message_spec.rb193
-rw-r--r--spec/models/integrations/confluence_spec.rb9
-rw-r--r--spec/models/integrations/custom_issue_tracker_spec.rb2
-rw-r--r--spec/models/integrations/drone_ci_spec.rb2
-rw-r--r--spec/models/integrations/microsoft_teams_spec.rb2
-rw-r--r--spec/models/integrations/prometheus_spec.rb8
-rw-r--r--spec/models/integrations/teamcity_spec.rb4
-rw-r--r--spec/models/integrations/unify_circuit_spec.rb2
-rw-r--r--spec/models/integrations/webex_teams_spec.rb2
-rw-r--r--spec/models/internal_id_spec.rb12
-rw-r--r--spec/models/issue_assignee_spec.rb8
-rw-r--r--spec/models/issue_spec.rb252
-rw-r--r--spec/models/label_link_spec.rb2
-rw-r--r--spec/models/lfs_objects_project_spec.rb2
-rw-r--r--spec/models/loose_foreign_keys/deleted_record_spec.rb2
-rw-r--r--spec/models/member_spec.rb17
-rw-r--r--spec/models/members/group_member_spec.rb9
-rw-r--r--spec/models/merge_request/diff_llm_summary_spec.rb18
-rw-r--r--spec/models/merge_request/metrics_spec.rb2
-rw-r--r--spec/models/merge_request_assignee_spec.rb4
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb4
-rw-r--r--spec/models/merge_request_diff_file_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb46
-rw-r--r--spec/models/milestone_spec.rb46
-rw-r--r--spec/models/ml/experiment_spec.rb15
-rw-r--r--spec/models/ml/model_spec.rb62
-rw-r--r--spec/models/ml/model_version_spec.rb90
-rw-r--r--spec/models/namespace/package_setting_spec.rb6
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb39
-rw-r--r--spec/models/namespace_spec.rb132
-rw-r--r--spec/models/oauth_access_token_spec.rb8
-rw-r--r--spec/models/organizations/organization_setting_spec.rb57
-rw-r--r--spec/models/organizations/organization_spec.rb21
-rw-r--r--spec/models/organizations/organization_user_spec.rb10
-rw-r--r--spec/models/packages/dependency_spec.rb4
-rw-r--r--spec/models/packages/maven/metadatum_spec.rb2
-rw-r--r--spec/models/packages/npm/metadatum_spec.rb4
-rw-r--r--spec/models/packages/package_spec.rb43
-rw-r--r--spec/models/pages/lookup_path_spec.rb15
-rw-r--r--spec/models/pages_deployment_spec.rb2
-rw-r--r--spec/models/pages_domain_spec.rb2
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb2
-rw-r--r--spec/models/performance_monitoring/prometheus_metric_spec.rb2
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_group_spec.rb2
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_spec.rb2
-rw-r--r--spec/models/personal_access_token_spec.rb14
-rw-r--r--spec/models/plan_limits_spec.rb172
-rw-r--r--spec/models/postgresql/detached_partition_spec.rb8
-rw-r--r--spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb4
-rw-r--r--spec/models/project_label_spec.rb4
-rw-r--r--spec/models/project_setting_spec.rb3
-rw-r--r--spec/models/project_spec.rb362
-rw-r--r--spec/models/project_statistics_spec.rb27
-rw-r--r--spec/models/projects/topic_spec.rb4
-rw-r--r--spec/models/projects/triggered_hooks_spec.rb39
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb77
-rw-r--r--spec/models/protected_tag/create_access_level_spec.rb130
-rw-r--r--spec/models/release_highlight_spec.rb16
-rw-r--r--spec/models/release_spec.rb4
-rw-r--r--spec/models/remote_mirror_spec.rb8
-rw-r--r--spec/models/route_spec.rb4
-rw-r--r--spec/models/service_desk_setting_spec.rb59
-rw-r--r--spec/models/todo_spec.rb2
-rw-r--r--spec/models/user_custom_attribute_spec.rb10
-rw-r--r--spec/models/user_preference_spec.rb3
-rw-r--r--spec/models/user_spec.rb206
-rw-r--r--spec/models/users/merge_request_interaction_spec.rb2
-rw-r--r--spec/models/users_statistics_spec.rb2
-rw-r--r--spec/models/wiki_directory_spec.rb8
-rw-r--r--spec/models/work_item_spec.rb81
-rw-r--r--spec/models/work_items/parent_link_spec.rb4
-rw-r--r--spec/models/work_items/type_spec.rb4
-rw-r--r--spec/models/work_items/widgets/base_spec.rb6
-rw-r--r--spec/models/work_items/widgets/current_user_todos_spec.rb60
-rw-r--r--spec/models/work_items/widgets/milestone_spec.rb2
-rw-r--r--spec/policies/global_policy_spec.rb54
-rw-r--r--spec/policies/group_policy_spec.rb166
-rw-r--r--spec/policies/merge_request_policy_spec.rb31
-rw-r--r--spec/policies/note_policy_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb217
-rw-r--r--spec/presenters/alert_management/alert_presenter_spec.rb18
-rw-r--r--spec/presenters/blob_presenter_spec.rb108
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb106
-rw-r--r--spec/presenters/ml/models_index_presenter_spec.rb33
-rw-r--r--spec/presenters/project_presenter_spec.rb29
-rw-r--r--spec/presenters/snippet_blob_presenter_spec.rb4
-rw-r--r--spec/requests/admin/users_controller_spec.rb20
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb2
-rw-r--r--spec/requests/api/admin/plan_limits_spec.rb10
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb2
-rw-r--r--spec/requests/api/ci/pipeline_schedules_spec.rb18
-rw-r--r--spec/requests/api/ci/variables_spec.rb7
-rw-r--r--spec/requests/api/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb29
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb39
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/discussions_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb68
-rw-r--r--spec/requests/api/error_tracking/project_settings_spec.rb60
-rw-r--r--spec/requests/api/files_spec.rb8
-rw-r--r--spec/requests/api/graphql/boards/board_list_issues_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb178
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb15
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb2
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb4
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb104
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb114
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb19
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb)17
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb)4
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb)4
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb)0
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb)44
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/ci/runner/create_spec.rb32
-rw-r--r--spec/requests/api/graphql/mutations/issues/update_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb85
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb1
-rw-r--r--spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jobs_spec.rb31
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb36
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb47
-rw-r--r--spec/requests/api/group_clusters_spec.rb2
-rw-r--r--spec/requests/api/group_export_spec.rb120
-rw-r--r--spec/requests/api/group_variables_spec.rb7
-rw-r--r--spec/requests/api/groups_spec.rb99
-rw-r--r--spec/requests/api/helpers_spec.rb6
-rw-r--r--spec/requests/api/import_github_spec.rb27
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb54
-rw-r--r--spec/requests/api/lint_spec.rb10
-rw-r--r--spec/requests/api/merge_requests_spec.rb73
-rw-r--r--spec/requests/api/ml_model_packages_spec.rb146
-rw-r--r--spec/requests/api/npm_group_packages_spec.rb30
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb27
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb99
-rw-r--r--spec/requests/api/project_attributes.yml3
-rw-r--r--spec/requests/api/project_clusters_spec.rb2
-rw-r--r--spec/requests/api/project_export_spec.rb124
-rw-r--r--spec/requests/api/project_hooks_spec.rb1
-rw-r--r--spec/requests/api/project_packages_spec.rb14
-rw-r--r--spec/requests/api/projects_spec.rb78
-rw-r--r--spec/requests/api/protected_branches_spec.rb13
-rw-r--r--spec/requests/api/protected_tags_spec.rb13
-rw-r--r--spec/requests/api/search_spec.rb12
-rw-r--r--spec/requests/api/settings_spec.rb105
-rw-r--r--spec/requests/api/statistics_spec.rb2
-rw-r--r--spec/requests/api/usage_data_spec.rb55
-rw-r--r--spec/requests/api/user_runners_spec.rb243
-rw-r--r--spec/requests/api/users_spec.rb165
-rw-r--r--spec/requests/git_http_spec.rb5
-rw-r--r--spec/requests/groups/observability_controller_spec.rb4
-rw-r--r--spec/requests/lfs_http_spec.rb2
-rw-r--r--spec/requests/openid_connect_spec.rb11
-rw-r--r--spec/requests/organizations/organizations_controller_spec.rb16
-rw-r--r--spec/requests/projects/alert_management_controller_spec.rb69
-rw-r--r--spec/requests/projects/incidents_controller_spec.rb (renamed from spec/controllers/projects/incidents_controller_spec.rb)12
-rw-r--r--spec/requests/projects/issues_controller_spec.rb7
-rw-r--r--spec/requests/projects/merge_requests/creations_spec.rb20
-rw-r--r--spec/requests/projects/merge_requests_controller_spec.rb1
-rw-r--r--spec/requests/projects/ml/candidates_controller_spec.rb23
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb27
-rw-r--r--spec/requests/projects/ml/models_controller_spec.rb67
-rw-r--r--spec/requests/projects/packages/package_files_controller_spec.rb2
-rw-r--r--spec/requests/projects/service_desk/custom_email_controller_spec.rb380
-rw-r--r--spec/requests/projects/service_desk_controller_spec.rb (renamed from spec/controllers/projects/service_desk_controller_spec.rb)37
-rw-r--r--spec/requests/projects/tracing_controller_spec.rb68
-rw-r--r--spec/requests/search_controller_spec.rb6
-rw-r--r--spec/requests/users_controller_spec.rb28
-rw-r--r--spec/requests/verifies_with_email_spec.rb2
-rw-r--r--spec/routing/organizations/organizations_controller_routing_spec.rb11
-rw-r--r--spec/rubocop/cop/avoid_return_from_blocks_spec.rb10
-rw-r--r--spec/rubocop/cop/background_migration/avoid_silent_rescue_exceptions_spec.rb107
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb50
-rw-r--r--spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb38
-rw-r--r--spec/rubocop/cop/graphql/gid_expected_type_spec.rb20
-rw-r--r--spec/rubocop/cop/graphql/id_type_spec.rb4
-rw-r--r--spec/rubocop/cop/ignored_columns_spec.rb8
-rw-r--r--spec/rubocop/cop/migration/avoid_finalize_background_migration_spec.rb21
-rw-r--r--spec/rubocop/cop/qa/element_with_pattern_spec.rb2
-rw-r--r--spec/rubocop/cop/rake/require_spec.rb108
-rw-r--r--spec/rubocop/cop/rspec/before_all_role_assignment_spec.rb234
-rw-r--r--spec/rubocop/cop/search/avoid_checking_finished_on_deprecated_migrations_spec.rb31
-rw-r--r--spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb52
-rw-r--r--spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb140
-rw-r--r--spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb76
-rw-r--r--spec/rubocop/cop/usage_data/large_table_spec.rb10
-rw-r--r--spec/rubocop/cop_todo_spec.rb15
-rw-r--r--spec/rubocop/formatter/todo_formatter_spec.rb41
-rw-r--r--spec/scripts/generate_failed_package_and_test_mr_message_spec.rb2
-rw-r--r--spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb2
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb24
-rw-r--r--spec/scripts/trigger-build_spec.rb1
-rw-r--r--spec/serializers/context_commits_diff_entity_spec.rb2
-rw-r--r--spec/serializers/diff_viewer_entity_spec.rb48
-rw-r--r--spec/serializers/environment_entity_spec.rb26
-rw-r--r--spec/serializers/issue_sidebar_basic_entity_spec.rb1
-rw-r--r--spec/serializers/lfs_file_lock_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_poll_widget_entity_spec.rb2
-rw-r--r--spec/serializers/prometheus_alert_entity_spec.rb22
-rw-r--r--spec/serializers/stage_entity_spec.rb6
-rw-r--r--spec/services/admin/plan_limits/update_service_spec.rb24
-rw-r--r--spec/services/alert_management/alerts/todo/create_service_spec.rb2
-rw-r--r--spec/services/application_settings/update_service_spec.rb9
-rw-r--r--spec/services/auth/dependency_proxy_authentication_service_spec.rb2
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb171
-rw-r--r--spec/services/auto_merge_service_spec.rb21
-rw-r--r--spec/services/award_emojis/add_service_spec.rb6
-rw-r--r--spec/services/award_emojis/base_service_spec.rb37
-rw-r--r--spec/services/award_emojis/destroy_service_spec.rb6
-rw-r--r--spec/services/boards/issues/list_service_spec.rb10
-rw-r--r--spec/services/boards/lists/list_service_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb36
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb28
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_skips_rollback_on_failure.yml47
-rw-r--r--spec/services/ci/pipeline_schedules/create_service_spec.rb86
-rw-r--r--spec/services/ci/pipeline_schedules/update_service_spec.rb55
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb4
-rw-r--r--spec/services/ci/process_sync_events_service_spec.rb4
-rw-r--r--spec/services/ci/register_job_service_spec.rb4
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb4
-rw-r--r--spec/services/clusters/agent_tokens/create_service_spec.rb15
-rw-r--r--spec/services/clusters/agents/authorize_proxy_user_service_spec.rb16
-rw-r--r--spec/services/clusters/integrations/prometheus_health_check_service_spec.rb115
-rw-r--r--spec/services/design_management/generate_image_versions_service_spec.rb2
-rw-r--r--spec/services/draft_notes/create_service_spec.rb14
-rw-r--r--spec/services/environments/create_service_spec.rb3
-rw-r--r--spec/services/environments/update_service_spec.rb15
-rw-r--r--spec/services/error_tracking/issue_details_service_spec.rb33
-rw-r--r--spec/services/error_tracking/issue_latest_event_service_spec.rb35
-rw-r--r--spec/services/error_tracking/issue_update_service_spec.rb40
-rw-r--r--spec/services/error_tracking/list_issues_service_spec.rb88
-rw-r--r--spec/services/git/base_hooks_service_spec.rb17
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb1
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb1
-rw-r--r--spec/services/groups/participants_service_spec.rb73
-rw-r--r--spec/services/groups/update_service_spec.rb14
-rw-r--r--spec/services/groups/update_shared_runners_service_spec.rb183
-rw-r--r--spec/services/import/github_service_spec.rb44
-rw-r--r--spec/services/import_csv/preprocess_milestones_service_spec.rb83
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb2
-rw-r--r--spec/services/integrations/group_mention_service_spec.rb143
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb2
-rw-r--r--spec/services/issuable/import_csv/base_service_spec.rb92
-rw-r--r--spec/services/issuable/process_assignees_spec.rb14
-rw-r--r--spec/services/issues/build_service_spec.rb1
-rw-r--r--spec/services/issues/create_service_spec.rb29
-rw-r--r--spec/services/issues/relative_position_rebalancing_service_spec.rb93
-rw-r--r--spec/services/issues/reopen_service_spec.rb6
-rw-r--r--spec/services/members/invite_service_spec.rb12
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb6
-rw-r--r--spec/services/merge_requests/cleanup_refs_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_orchestration_service_spec.rb18
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb12
-rw-r--r--spec/services/merge_requests/mergeability_check_batch_service_spec.rb46
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb45
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb4
-rw-r--r--spec/services/merge_requests/update_service_spec.rb29
-rw-r--r--spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb2
-rw-r--r--spec/services/milestones/create_service_spec.rb68
-rw-r--r--spec/services/milestones/update_service_spec.rb90
-rw-r--r--spec/services/namespace_settings/update_service_spec.rb16
-rw-r--r--spec/services/notes/post_process_service_spec.rb3
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb4
-rw-r--r--spec/services/notification_service_spec.rb4
-rw-r--r--spec/services/packages/cleanup/execute_policy_service_spec.rb4
-rw-r--r--spec/services/packages/debian/find_or_create_package_service_spec.rb83
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb140
-rw-r--r--spec/services/packages/npm/create_metadata_cache_service_spec.rb2
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb14
-rw-r--r--spec/services/packages/npm/generate_metadata_service_spec.rb6
-rw-r--r--spec/services/packages/nuget/extract_metadata_content_service_spec.rb64
-rw-r--r--spec/services/packages/nuget/extract_metadata_file_service_spec.rb100
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb138
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb2
-rw-r--r--spec/services/personal_access_tokens/last_used_service_spec.rb44
-rw-r--r--spec/services/personal_access_tokens/revoke_token_family_service_spec.rb18
-rw-r--r--spec/services/personal_access_tokens/rotate_service_spec.rb7
-rw-r--r--spec/services/projects/create_service_spec.rb30
-rw-r--r--spec/services/projects/destroy_service_spec.rb61
-rw-r--r--spec/services/projects/download_service_spec.rb4
-rw-r--r--spec/services/projects/participants_service_spec.rb16
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/services/prometheus/proxy_variable_substitution_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb25
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb16
-rw-r--r--spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb2
-rw-r--r--spec/services/service_desk/custom_emails/create_service_spec.rb185
-rw-r--r--spec/services/service_desk/custom_emails/destroy_service_spec.rb95
-rw-r--r--spec/services/service_desk_settings/update_service_spec.rb14
-rw-r--r--spec/services/service_response_spec.rb13
-rw-r--r--spec/services/snippets/update_service_spec.rb2
-rw-r--r--spec/services/spam/spam_action_service_spec.rb3
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb61
-rw-r--r--spec/services/system_notes/time_tracking_service_spec.rb12
-rw-r--r--spec/services/test_hooks/project_service_spec.rb21
-rw-r--r--spec/services/todo_service_spec.rb51
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb4
-rw-r--r--spec/services/users/allow_possible_spam_service_spec.rb24
-rw-r--r--spec/services/users/ban_service_spec.rb7
-rw-r--r--spec/services/users/disallow_possible_spam_service_spec.rb34
-rw-r--r--spec/services/web_hook_service_spec.rb11
-rw-r--r--spec/services/webauthn/authenticate_service_spec.rb6
-rw-r--r--spec/services/webauthn/register_service_spec.rb4
-rw-r--r--spec/services/work_items/export_csv_service_spec.rb2
-rw-r--r--spec/services/work_items/update_service_spec.rb36
-rw-r--r--spec/services/work_items/widgets/current_user_todos_service/update_service_spec.rb2
-rw-r--r--spec/simplecov_env.rb57
-rw-r--r--spec/spec_helper.rb39
-rw-r--r--spec/support/ability_check.rb2
-rw-r--r--spec/support/capybara.rb4
-rw-r--r--spec/support/database/click_house/hooks.rb55
-rw-r--r--spec/support/database/prevent_cross_joins.rb2
-rw-r--r--spec/support/db_cleaner.rb2
-rw-r--r--spec/support/finder_collection_allowlist.yml1
-rw-r--r--spec/support/formatters/json_formatter.rb13
-rw-r--r--spec/support/helpers/content_editor_helpers.rb10
-rw-r--r--spec/support/helpers/database/migration_testing_helpers.rb6
-rw-r--r--spec/support/helpers/database/multiple_databases_helpers.rb2
-rw-r--r--spec/support/helpers/emails_helper_test_helper.rb2
-rw-r--r--spec/support/helpers/features/autocomplete_helpers.rb13
-rw-r--r--spec/support/helpers/features/invite_members_modal_helpers.rb2
-rw-r--r--spec/support/helpers/features/iteration_helpers.rb2
-rw-r--r--spec/support/helpers/gitaly_setup.rb3
-rw-r--r--spec/support/helpers/graphql_helpers.rb2
-rw-r--r--spec/support/helpers/ldap_helpers.rb2
-rw-r--r--spec/support/helpers/next_found_instance_of.rb2
-rw-r--r--spec/support/helpers/next_instance_of.rb5
-rw-r--r--spec/support/helpers/redis_helpers.rb18
-rw-r--r--spec/support/helpers/reload_helpers.rb5
-rw-r--r--spec/support/helpers/require_migration.rb2
-rw-r--r--spec/support/helpers/stub_env.rb52
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--spec/support/helpers/usage_data_helpers.rb2
-rw-r--r--spec/support/helpers/wait_for_requests.rb17
-rw-r--r--spec/support/import_export/configuration_helper.rb2
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/matchers/exceed_query_limit.rb2
-rw-r--r--spec/support/matchers/have_native_text_validation_message.rb8
-rw-r--r--spec/support/matchers/result_matchers.rb66
-rw-r--r--spec/support/rspec.rb4
-rw-r--r--spec/support/rspec_order_todo.yml23
-rw-r--r--spec/support/shared_contexts/email_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/features/integrations/integrations_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb14
-rw-r--r--spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb21
-rw-r--r--spec/support/shared_contexts/merge_request_create_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/merge_request_edit_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_contexts/prometheus/alert_shared_context.rb11
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_metadata_shared_examples.rb40
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/user_contribution_events_shared_context.rb37
-rw-r--r--spec/support/shared_examples/ci/stage_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/controllers/internal_event_tracking_examples.rb50
-rw-r--r--spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/cascading_settings_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb57
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/inviting_members_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/features/milestone_editing_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/nav_sidebar_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/project_upload_files_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/finders/assignees_filter_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/label_fields.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb48
-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/table_validators_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb30
-rw-r--r--spec/support/shared_examples/models/concerns/protected_ref_deploy_key_access_examples.rb132
-rw-r--r--spec/support/shared_examples/models/database_event_tracking_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/namespaces/traversal_examples.rb8
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb7
-rw-r--r--spec/support/shared_examples/nav_sidebar_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/npm_sync_metadata_cache_worker_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/observability/csp_shared_examples.rb172
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb50
-rw-r--r--spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/requests/response_status_with_error_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/services/auto_merge_shared_examples.rb182
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/views/nav_sidebar_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/views/pipeline_status_changes_email.rb8
-rw-r--r--spec/support/shared_examples/views/preferred_language.rb19
-rw-r--r--spec/support/stub_dot_com_check.rb2
-rw-r--r--spec/support/system_exit_detected.rb3
-rw-r--r--spec/support/time_travel.rb21
-rw-r--r--spec/support/webmock.rb9
-rw-r--r--spec/support_specs/helpers/graphql_helpers_spec.rb2
-rw-r--r--spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb4
-rw-r--r--spec/support_specs/matchers/result_matchers_spec.rb21
-rw-r--r--spec/support_specs/time_travel_spec.rb21
-rw-r--r--spec/tasks/dev_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/feature_categories_rake_spec.rb3
-rw-r--r--spec/tasks/gitlab/metrics_exporter_rake_spec.rb81
-rw-r--r--spec/tasks/gitlab/packages/events_rake_spec.rb55
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb72
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb17
-rw-r--r--spec/tooling/danger/experiments_spec.rb59
-rw-r--r--spec/tooling/danger/project_helper_spec.rb12
-rw-r--r--spec/tooling/lib/tooling/find_changes_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/find_tests_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/gettext_extractor_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/predictive_tests_spec.rb2
-rw-r--r--spec/tooling/quality/test_level_spec.rb2
-rw-r--r--spec/tooling/rspec_flaky/config_spec.rb106
-rw-r--r--spec/tooling/rspec_flaky/example_spec.rb99
-rw-r--r--spec/tooling/rspec_flaky/flaky_example_spec.rb148
-rw-r--r--spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb85
-rw-r--r--spec/tooling/rspec_flaky/listener_spec.rb228
-rw-r--r--spec/tooling/rspec_flaky/report_spec.rb138
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb6
-rw-r--r--spec/uploaders/design_management/design_v432x230_uploader_spec.rb6
-rw-r--r--spec/uploaders/favicon_uploader_spec.rb6
-rw-r--r--spec/validators/cron_validator_spec.rb4
-rw-r--r--spec/validators/html_safety_validator_spec.rb2
-rw-r--r--spec/views/admin/application_settings/_slack.html.haml_spec.rb42
-rw-r--r--spec/views/admin/application_settings/general.html.haml_spec.rb5
-rw-r--r--spec/views/explore/projects/topic.html.haml_spec.rb24
-rw-r--r--spec/views/groups/packages/index.html.haml_spec.rb18
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb8
-rw-r--r--spec/views/layouts/application.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/devise.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/devise_empty.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/fullscreen.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/signup_onboarding.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/simple_registration.html.haml_spec.rb7
-rw-r--r--spec/views/layouts/terms.html.haml_spec.rb1
-rw-r--r--spec/views/notify/approved_merge_request_email.html.haml_spec.rb2
-rw-r--r--spec/views/notify/import_issues_csv_email.html.haml_spec.rb29
-rw-r--r--spec/views/notify/pipeline_failed_email.html.haml_spec.rb17
-rw-r--r--spec/views/notify/pipeline_failed_email.text.erb_spec.rb55
-rw-r--r--spec/views/notify/pipeline_fixed_email.html.haml_spec.rb17
-rw-r--r--spec/views/notify/pipeline_fixed_email.text.erb_spec.rb17
-rw-r--r--spec/views/notify/pipeline_success_email.html.haml_spec.rb17
-rw-r--r--spec/views/notify/pipeline_success_email.text.erb_spec.rb17
-rw-r--r--spec/views/profiles/keys/_key_details.html.haml_spec.rb15
-rw-r--r--spec/views/profiles/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb25
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb22
-rw-r--r--spec/views/projects/packages/index.html.haml_spec.rb18
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb1
-rw-r--r--spec/views/projects/runners/_project_runners.html.haml_spec.rb59
-rw-r--r--spec/views/registrations/welcome/show.html.haml_spec.rb2
-rw-r--r--spec/views/search/_results.html.haml_spec.rb3
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb30
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb30
-rw-r--r--spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb75
-rw-r--r--spec/workers/bulk_imports/pipeline_batch_worker_spec.rb136
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb46
-rw-r--r--spec/workers/bulk_imports/relation_export_worker_spec.rb24
-rw-r--r--spec/workers/ci/merge_requests/cleanup_ref_worker_spec.rb47
-rw-r--r--spec/workers/ci/pipeline_cleanup_ref_worker_spec.rb61
-rw-r--r--spec/workers/container_registry/cleanup_worker_spec.rb19
-rw-r--r--spec/workers/container_registry/record_data_repair_detail_worker_spec.rb11
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb39
-rw-r--r--spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb2
-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_notes_worker_spec.rb2
-rw-r--r--spec/workers/incident_management/close_incident_worker_spec.rb2
-rw-r--r--spec/workers/integrations/execute_worker_spec.rb14
-rw-r--r--spec/workers/integrations/group_mention_worker_spec.rb65
-rw-r--r--spec/workers/jira_connect/retry_request_worker_spec.rb6
-rw-r--r--spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb54
-rw-r--r--spec/workers/packages/debian/process_changes_worker_spec.rb133
-rw-r--r--spec/workers/packages/nuget/extraction_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb14
-rw-r--r--spec/workers/redis_migration_worker_spec.rb73
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb23
1720 files changed, 35358 insertions, 34602 deletions
diff --git a/spec/commands/metrics_server/metrics_server_spec.rb b/spec/commands/metrics_server/metrics_server_spec.rb
index 88a28b02903..ee07602016f 100644
--- a/spec/commands/metrics_server/metrics_server_spec.rb
+++ b/spec/commands/metrics_server/metrics_server_spec.rb
@@ -30,18 +30,6 @@ RSpec.describe 'GitLab metrics server', :aggregate_failures do
}
end
- before(:all) do
- Rake.application.rake_require 'tasks/gitlab/metrics_exporter'
-
- @exporter_path = Rails.root.join('tmp', 'test', 'gme')
-
- run_rake_task('gitlab:metrics_exporter:install', @exporter_path)
- end
-
- after(:all) do
- FileUtils.rm_rf(@exporter_path)
- end
-
shared_examples 'serves metrics endpoint' do
it 'serves /metrics endpoint' do
start_server!
@@ -59,24 +47,18 @@ RSpec.describe 'GitLab metrics server', :aggregate_failures do
end
end
- shared_examples 'spawns a server' do |target, use_golang_server|
- context "targeting #{target} when using Golang server is #{use_golang_server}" do
+ shared_examples 'spawns a server' do |target|
+ context "targeting #{target}" do
let(:metrics_dir) { Dir.mktmpdir }
subject(:start_server!) do
- @pid = MetricsServer.spawn(target, metrics_dir: metrics_dir, path: @exporter_path.join('bin'))
+ @pid = MetricsServer.spawn(target, metrics_dir: metrics_dir)
end
before do
- if use_golang_server
- stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
- allow(Settings).to receive(:monitoring).and_return(
- GitlabSettings::Options.build(config.dig('test', 'monitoring')))
- else
- config_file.write(YAML.dump(config))
- config_file.close
- stub_env('GITLAB_CONFIG', config_file.path)
- end
+ config_file.write(YAML.dump(config))
+ config_file.close
+ stub_env('GITLAB_CONFIG', config_file.path)
# We need to send a request to localhost
WebMock.allow_net_connect!
end
@@ -111,8 +93,6 @@ RSpec.describe 'GitLab metrics server', :aggregate_failures do
end
end
- it_behaves_like 'spawns a server', 'puma', true
- it_behaves_like 'spawns a server', 'puma', false
- it_behaves_like 'spawns a server', 'sidekiq', true
- it_behaves_like 'spawns a server', 'sidekiq', false
+ it_behaves_like 'spawns a server', 'puma'
+ it_behaves_like 'spawns a server', 'sidekiq'
end
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 085be1ceac2..a63e7158c2a 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -247,13 +247,13 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, feature_category: :gitlab_cli, stub_
expected_workers =
if Gitlab.ee?
[
- %w[cronjob:clusters_integrations_check_prometheus_health incident_management_close_incident status_page_publish] + described_class::DEFAULT_QUEUES,
+ %w[incident_management_close_incident status_page_publish] + described_class::DEFAULT_QUEUES,
%w[bulk_imports_pipeline bulk_imports_relation_export project_export projects_import_export_parallel_project_export projects_import_export_relation_export repository_import project_template_export] +
described_class::DEFAULT_QUEUES
]
else
[
- %w[cronjob:clusters_integrations_check_prometheus_health incident_management_close_incident] + described_class::DEFAULT_QUEUES,
+ %w[incident_management_close_incident] + described_class::DEFAULT_QUEUES,
%w[bulk_imports_pipeline bulk_imports_relation_export project_export projects_import_export_parallel_project_export projects_import_export_relation_export repository_import] +
described_class::DEFAULT_QUEUES
]
diff --git a/spec/components/pajamas/empty_state_component_spec.rb b/spec/components/pajamas/empty_state_component_spec.rb
new file mode 100644
index 00000000000..5aa3f2143c3
--- /dev/null
+++ b/spec/components/pajamas/empty_state_component_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Pajamas::EmptyStateComponent, type: :component, feature_category: :design_system do
+ let(:title) { 'Empty state title' }
+ let(:primary_button_link) { '#learn-more-primary' }
+ let(:primary_button_text) { 'Learn more' }
+ let(:secondary_button_link) { '#learn-more-secondary' }
+ let(:secondary_button_text) { 'Another action' }
+ let(:description) { 'Empty state description' }
+ let(:svg_path) { 'illustrations/empty-state/empty-projects-deleted-md.svg' }
+ let(:compact) { false }
+ let(:empty_state_options) { { id: 'empty-state-rails-component' } }
+
+ before do
+ render_inline described_class.new(
+ title: title,
+ svg_path: svg_path,
+ empty_state_options: empty_state_options,
+ primary_button_link: primary_button_link,
+ primary_button_text: primary_button_text,
+ secondary_button_link: secondary_button_link,
+ secondary_button_text: secondary_button_text,
+ compact: compact) do |c|
+ c.with_description { description } if description
+ end
+ end
+
+ describe 'default' do
+ it 'renders the primary action' do
+ expect(page).to have_link(primary_button_text, href: primary_button_link)
+ end
+
+ it 'renders the secondary action' do
+ expect(page).to have_link(secondary_button_text, href: secondary_button_link)
+ end
+
+ it 'renders image as illustration' do
+ img = page.find('img')
+
+ expect(img['src']).to eq(ActionController::Base.helpers.image_path(svg_path))
+ end
+
+ it 'renders title' do
+ h1 = page.find('h1')
+
+ expect(h1).to have_text(title)
+ end
+
+ it 'renders description' do
+ expect(find_description).to have_text(description)
+ end
+
+ it 'renders section with flex direction column' do
+ expect(find_section[:id]).to eq(empty_state_options[:id])
+ expect(find_section[:class]).to eq("gl-display-flex empty-state gl-text-center gl-flex-direction-column")
+ end
+ end
+
+ describe 'when compact' do
+ let(:compact) { true }
+
+ it 'renders section with flex direction row' do
+ expect(find_section[:class]).to eq("gl-display-flex empty-state gl-flex-direction-row gl-align-items-center")
+ end
+ end
+
+ describe 'when svg_path is empty' do
+ let(:svg_path) { '' }
+
+ it 'does not render image' do
+ expect(page).not_to have_selector('img')
+ end
+ end
+
+ describe 'when description is empty' do
+ let(:description) { nil }
+
+ it 'does not render a description' do
+ expect(find_description).to be_nil
+ end
+ end
+
+ describe 'with no buttons' do
+ let(:primary_button_text) { nil }
+ let(:secondary_button_text) { nil }
+
+ it 'does not render any buttons' do
+ expect(page).not_to have_selector('a')
+ end
+ end
+
+ def find_section
+ page.find('section')
+ end
+
+ def find_description
+ page.first('[data-testid="empty-state-description"]', minimum: 0)
+ end
+end
diff --git a/spec/components/previews/pajamas/empty_state_component_preview.rb b/spec/components/previews/pajamas/empty_state_component_preview.rb
new file mode 100644
index 00000000000..bad00571026
--- /dev/null
+++ b/spec/components/previews/pajamas/empty_state_component_preview.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Pajamas
+ class EmptyStateComponentPreview < ViewComponent::Preview
+ # @param title text
+ # @param description textarea
+ # @param compact toggle
+ # @param svg_path text
+ # @param primary_button_text text
+ # @param primary_button_link text
+ # @param secondary_button_text text
+ # @param secondary_button_link text
+ def default(
+ title: "This state is empty",
+ description: "The title and message should be clear, concise, and explain why the user is seeing this screen.
+ The actions should help the user on what to do to get the real feature.",
+ compact: false,
+ svg_path: "illustrations/empty-state/empty-projects-deleted-md.svg",
+ primary_button_text: "Do primary action",
+ primary_button_link: "#learn-more-primary",
+ secondary_button_text: "Do secondary action",
+ secondary_button_link: "#learn-more-secondary")
+ render(Pajamas::EmptyStateComponent.new(
+ title: title,
+ svg_path: svg_path,
+ primary_button_text: primary_button_text,
+ primary_button_link: primary_button_link,
+ secondary_button_text: secondary_button_text,
+ secondary_button_link: secondary_button_link,
+ compact: compact
+ )) do |c|
+ c.with_description { description } if description
+ end
+ end
+ end
+end
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index 55e675d5107..4639e533922 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -170,12 +170,12 @@ RSpec.describe Settings, feature_category: :system_access do
it 'defaults to using the encrypted_settings_key_base for the key' do
expect(Gitlab::EncryptedConfiguration).to receive(:new).with(hash_including(base_key: Gitlab::Application.secrets.encrypted_settings_key_base))
- Settings.encrypted('tmp/tests/test.enc')
+ described_class.encrypted('tmp/tests/test.enc')
end
it 'returns empty encrypted config when a key has not been set' do
allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
- expect(Settings.encrypted('tmp/tests/test.enc').read).to be_empty
+ expect(described_class.encrypted('tmp/tests/test.enc').read).to be_empty
end
end
diff --git a/spec/contracts/provider/spec_helper.rb b/spec/contracts/provider/spec_helper.rb
index 44e4d29c18e..f1ceca16b4b 100644
--- a/spec/contracts/provider/spec_helper.rb
+++ b/spec/contracts/provider/spec_helper.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
require 'zeitwerk'
+require 'gitlab/rspec/all'
require_relative 'helpers/users_helper'
require_relative('../../../ee/spec/contracts/provider/spec_helper') if Gitlab.ee?
require Rails.root.join("spec/support/helpers/rails_helpers.rb")
-require Rails.root.join("spec/support/helpers/stub_env.rb")
# Opt out of telemetry collection. We can't allow all engineers, and users who install GitLab from source, to be
# automatically enrolled in sending data on their usage without their knowledge.
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 537424093fb..60343c822af 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -487,6 +487,43 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
end
end
+ describe 'GET #slack_app_manifest_download', feature_category: :integrations do
+ before do
+ sign_in(admin)
+ end
+
+ subject { get :slack_app_manifest_download }
+
+ it 'downloads the GitLab for Slack app manifest' do
+ allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' })
+
+ subject
+
+ expect(response.body).to eq('{"foo":"bar"}')
+ expect(response.headers['Content-Disposition']).to eq(
+ 'attachment; filename="slack_manifest.json"; filename*=UTF-8\'\'slack_manifest.json'
+ )
+ end
+ end
+
+ describe 'GET #slack_app_manifest_share', feature_category: :integrations do
+ before do
+ sign_in(admin)
+ end
+
+ subject { get :slack_app_manifest_share }
+
+ it 'redirects the user to the Slack Manifest share URL' do
+ allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' })
+
+ subject
+
+ expect(response).to redirect_to(
+ "https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D"
+ )
+ end
+ end
+
describe 'GET #service_usage_data', feature_category: :service_ping do
before do
stub_usage_data_connections
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index b1a2d90589a..6fa8d2c61c1 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -41,18 +41,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
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_for_admin: false)
- end
-
- it 'returns :not_found' do
- get :new
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
describe '#register' do
@@ -78,20 +66,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
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_for_admin: false)
- end
-
- it 'returns :not_found' do
- register
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
describe '#edit' do
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 58125f3a831..0beaae7a2d7 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -567,20 +567,6 @@ RSpec.describe ApplicationController, feature_category: :shared do
expect(controller.last_payload[:response_bytes]).to eq('authenticated'.bytesize)
end
-
- context 'with log_response_length disabled' do
- before do
- stub_feature_flags(log_response_length: false)
- end
-
- it 'logs response length' do
- sign_in user
-
- get :index
-
- expect(controller.last_payload).not_to include(:response_bytes)
- end
- end
end
describe '#access_denied' do
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index 6fa273bf3d7..9eb0f36cb37 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe IssuableCollections do
}
end
- it 'only allows whitelisted params' do
+ it 'only allows allowlisted params' do
is_expected.to include({
'assignee_id' => '1',
'assignee_username' => 'user1',
@@ -123,7 +123,7 @@ RSpec.describe IssuableCollections do
}
end
- it 'only allows whitelisted params' do
+ it 'only allows allowlisted params' do
is_expected.to include({
'label_name' => %w[label1 label2],
'assignee_username' => %w[user1 user2]
diff --git a/spec/controllers/concerns/kas_cookie_spec.rb b/spec/controllers/concerns/kas_cookie_spec.rb
index 7ab48f12d83..c9490508690 100644
--- a/spec/controllers/concerns/kas_cookie_spec.rb
+++ b/spec/controllers/concerns/kas_cookie_spec.rb
@@ -42,14 +42,6 @@ RSpec.describe KasCookie, feature_category: :deployment_management do
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
@@ -88,60 +80,42 @@ RSpec.describe KasCookie, feature_category: :deployment_management do
request.env['action_dispatch.content_security_policy'].directives['connect-src']
end
- context "when feature flag is disabled" do
+ context 'when KAS is on same domain as rails' do
let_it_be(:kas_tunnel_url) { 'ws://gitlab.example.com/-/k8s-proxy/' }
- before do
- stub_feature_flags(kas_user_access: false)
- end
-
- it 'does not add KAS url to connect-src directives' do
+ it 'does not add KAS url to CSP connect-src directive' do
expect(kas_csp_connect_src).not_to include(::Gitlab::Kas.tunnel_url)
end
end
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(kas_user_access: true)
+ context 'when KAS is on subdomain' do
+ let_it_be(:kas_tunnel_url) { 'ws://kas.gitlab.example.com/k8s-proxy/' }
+
+ it 'adds KAS url to CSP connect-src directive' do
+ expect(kas_csp_connect_src).to include(::Gitlab::Kas.tunnel_url)
end
- context 'when KAS is on same domain as rails' do
- let_it_be(:kas_tunnel_url) { 'ws://gitlab.example.com/-/k8s-proxy/' }
+ context 'when content_security_policy is disabled' do
+ let(:content_security_policy_enabled) { false }
it 'does not add KAS url to CSP connect-src directive' do
expect(kas_csp_connect_src).not_to include(::Gitlab::Kas.tunnel_url)
end
end
+ end
- context 'when KAS is on subdomain' do
- let_it_be(:kas_tunnel_url) { 'ws://kas.gitlab.example.com/k8s-proxy/' }
-
- it 'adds KAS url to CSP connect-src directive' do
- expect(kas_csp_connect_src).to include(::Gitlab::Kas.tunnel_url)
- end
-
- context 'when content_security_policy is disabled' do
- let(:content_security_policy_enabled) { false }
+ context 'when KAS tunnel url is configured without trailing slash' do
+ let_it_be(:kas_tunnel_url) { 'ws://kas.gitlab.example.com/k8s-proxy' }
- it 'does not add KAS url to CSP connect-src directive' do
- expect(kas_csp_connect_src).not_to include(::Gitlab::Kas.tunnel_url)
- end
- end
+ it 'adds KAS url to CSP connect-src directive with trailing slash' do
+ expect(kas_csp_connect_src).to include("#{::Gitlab::Kas.tunnel_url}/")
end
- context 'when KAS tunnel url is configured without trailing slash' do
- let_it_be(:kas_tunnel_url) { 'ws://kas.gitlab.example.com/k8s-proxy' }
+ context 'when content_security_policy is disabled' do
+ let(:content_security_policy_enabled) { false }
- it 'adds KAS url to CSP connect-src directive with trailing slash' do
- expect(kas_csp_connect_src).to include("#{::Gitlab::Kas.tunnel_url}/")
- end
-
- context 'when content_security_policy is disabled' do
- let(:content_security_policy_enabled) { false }
-
- it 'does not add KAS url to CSP connect-src directive' do
- expect(kas_csp_connect_src).not_to include("#{::Gitlab::Kas.tunnel_url}/")
- end
+ it 'does not add KAS url to CSP connect-src directive' do
+ expect(kas_csp_connect_src).not_to include("#{::Gitlab::Kas.tunnel_url}/")
end
end
end
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
deleted file mode 100644
index 4a9c7c493a7..00000000000
--- a/spec/controllers/concerns/metrics_dashboard_spec.rb
+++ /dev/null
@@ -1,195 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MetricsDashboard, feature_category: :metrics do
- include MetricsDashboardHelpers
-
- describe 'GET #metrics_dashboard' do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { project_with_dashboard('.gitlab/dashboards/test.yml') }
- let_it_be(:environment) { create(:environment, project: project) }
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- sign_in(user)
- project.add_maintainer(user)
- end
-
- controller(::ApplicationController) do
- include MetricsDashboard
- end
-
- let(:json_response) do
- routes.draw { get "metrics_dashboard" => "anonymous#metrics_dashboard" }
- response = get :metrics_dashboard, format: :json
-
- response.parsed_body
- end
-
- context 'when no parameters are provided' do
- it 'returns an error json_response' do
- expect(json_response['status']).to eq('error')
- end
- end
-
- context 'when params are provided' do
- let(:params) { { environment: environment } }
-
- before do
- allow(controller).to receive(:project).and_return(project)
- allow(controller)
- .to receive(:metrics_dashboard_params)
- .and_return(params)
- end
-
- it 'returns the specified dashboard' do
- expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
- expect(json_response).not_to have_key('all_dashboards')
- expect(json_response).to have_key('metrics_data')
- end
-
- context 'when the params are in an alternate format' do
- let(:params) { ActionController::Parameters.new({ environment: environment }).permit! }
-
- it 'returns the specified dashboard' do
- expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
- expect(json_response).not_to have_key('all_dashboards')
- expect(json_response).to have_key('metrics_data')
- end
- end
-
- context 'when environment for dashboard is available' do
- let(:params) { { environment: environment } }
-
- before do
- allow(controller).to receive(:project).and_return(project)
- allow(controller).to receive(:environment).and_return(environment)
- allow(controller)
- .to receive(:metrics_dashboard_params)
- .and_return(params)
- end
-
- it 'returns the specified dashboard' do
- expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
- expect(json_response).not_to have_key('all_dashboards')
- expect(json_response).to have_key('metrics_data')
- end
- end
-
- context 'when dashboard path includes encoded characters' do
- let(:params) { { dashboard_path: 'dashboard%26copy.yml' } }
-
- before do
- allow(controller)
- .to receive(:metrics_dashboard_params)
- .and_return(params)
- end
-
- it 'decodes dashboard path' do
- expect(::Gitlab::Metrics::Dashboard::Finder).to receive(:find).with(anything, anything, hash_including(dashboard_path: 'dashboard&copy.yml'))
-
- json_response
- end
- end
-
- context 'when parameters are provided and the list of all dashboards is required' do
- before do
- allow(controller).to receive(:include_all_dashboards?).and_return(true)
- end
-
- it 'returns a dashboard in addition to the list of dashboards' do
- expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
- expect(json_response).to have_key('all_dashboards')
- end
-
- context 'in all_dashboard list' do
- let(:system_dashboard) { json_response['all_dashboards'].find { |dashboard| dashboard["system_dashboard"] == true } }
-
- let(:project_dashboard) do
- json_response['all_dashboards'].find do |dashboard|
- dashboard['path'] == '.gitlab/dashboards/test.yml'
- end
- end
-
- it 'includes project_blob_path only for project dashboards' do
- expect(system_dashboard['project_blob_path']).to be_nil
- expect(project_dashboard['project_blob_path']).to eq("/#{project.namespace.path}/#{project.path}/-/blob/master/.gitlab/dashboards/test.yml")
- end
-
- it 'allows editing only for project dashboards' do
- expect(system_dashboard['can_edit']).to be(false)
- expect(project_dashboard['can_edit']).to be(true)
- end
-
- it 'includes out_of_the_box_dashboard key' do
- expect(system_dashboard['out_of_the_box_dashboard']).to be(true)
- expect(project_dashboard['out_of_the_box_dashboard']).to be(false)
- end
-
- describe 'project permissions' do
- using RSpec::Parameterized::TableSyntax
-
- where(:can_collaborate, :system_can_edit, :project_can_edit) do
- false | false | false
- true | false | true
- end
-
- with_them do
- before do
- allow(controller).to receive(:can_collaborate_with_project?).and_return(can_collaborate)
- end
-
- it "sets can_edit appropriately" do
- expect(system_dashboard["can_edit"]).to eq(system_can_edit)
- expect(project_dashboard["can_edit"]).to eq(project_can_edit)
- end
- end
- end
-
- context 'starred dashboards' do
- let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
- let_it_be(:dashboards) do
- {
- '.gitlab/dashboards/test.yml' => dashboard_yml,
- '.gitlab/dashboards/anomaly.yml' => dashboard_yml,
- '.gitlab/dashboards/errors.yml' => dashboard_yml
- }
- end
-
- let_it_be(:project) { create(:project, :custom_repo, files: dashboards) }
-
- before do
- create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/errors.yml')
- create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/test.yml')
- end
-
- it 'adds starred dashboard information and sorts the list' do
- all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') }
- expected_response = [
- { "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/anomaly.yml' }) },
- { "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/errors.yml' }) },
- { "display_name" => "K8s pod health", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/pod_metrics.yml' }) },
- { "display_name" => "Overview", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/common_metrics.yml' }) },
- { "display_name" => "test.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/test.yml' }) }
- ]
-
- expect(all_dashboards).to eq(expected_response)
- end
- end
- end
- end
- end
-
- context 'when metrics dashboard feature is unavailable' do
- it 'returns 404 not found' do
- stub_feature_flags(remove_monitor_metrics: true)
-
- routes.draw { get "metrics_dashboard" => "anonymous#metrics_dashboard" }
- response = get :metrics_dashboard, format: :json
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-end
diff --git a/spec/controllers/concerns/onboarding/status_spec.rb b/spec/controllers/concerns/onboarding/status_spec.rb
new file mode 100644
index 00000000000..3f6e597a235
--- /dev/null
+++ b/spec/controllers/concerns/onboarding/status_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Onboarding::Status, feature_category: :onboarding do
+ let_it_be(:member) { create(:group_member) }
+ let_it_be(:user) { member.user }
+ let_it_be(:tasks_to_be_done) { %w[ci code] }
+ let_it_be(:source) { member.group }
+
+ describe '#continue_full_onboarding?' do
+ subject { described_class.new(nil).continue_full_onboarding? }
+
+ it { is_expected.to eq(false) }
+ end
+
+ describe '#single_invite?' do
+ subject { described_class.new(user).single_invite? }
+
+ context 'when there is only one member for the user' do
+ context 'when the member source exists' do
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ context 'when there is more than one member for the user' do
+ before do
+ create(:group_member, user: user)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when there are no members for the user' do
+ let(:user) { build_stubbed(:user) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#last_invited_member' do
+ subject { described_class.new(user).last_invited_member }
+
+ it { is_expected.to eq(member) }
+
+ context 'when another member exists and is most recent' do
+ let!(:last_member) { create(:group_member, user: user) }
+
+ it { is_expected.to eq(last_member) }
+ end
+
+ context 'when there are no members' do
+ let_it_be(:user) { build_stubbed(:user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#last_invited_member_source' do
+ subject { described_class.new(user).last_invited_member_source }
+
+ context 'when a member exists' do
+ it { is_expected.to eq(source) }
+ end
+
+ context 'when no members exist' do
+ let_it_be(:user) { build_stubbed(:user) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when another member exists and is most recent' do
+ let!(:last_member_source) { create(:group_member, user: user).group }
+
+ it { is_expected.to eq(last_member_source) }
+ end
+ end
+
+ describe '#invite_with_tasks_to_be_done?' do
+ subject { described_class.new(user).invite_with_tasks_to_be_done? }
+
+ context 'when there are tasks_to_be_done with one member' do
+ let_it_be(:member) { create(:group_member, user: user, tasks_to_be_done: tasks_to_be_done) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when there are multiple members and the tasks_to_be_done is on only one of them' do
+ before do
+ create(:group_member, user: user, tasks_to_be_done: tasks_to_be_done)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when there are no tasks_to_be_done' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when there are no members' do
+ let_it_be(:user) { build_stubbed(:user) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index 304e08f40bd..52f6f08e17a 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -16,22 +16,7 @@ RSpec.describe DashboardController, feature_category: :code_review_workflow do
end
describe 'GET issues' do
- context 'when issues_full_text_search is disabled' do
- before do
- stub_feature_flags(issues_full_text_search: false)
- end
-
- it_behaves_like 'issuables list meta-data', :issue, :issues
- end
-
- context 'when issues_full_text_search is enabled' do
- before do
- stub_feature_flags(issues_full_text_search: true)
- end
-
- it_behaves_like 'issuables list meta-data', :issue, :issues
- end
-
+ it_behaves_like 'issuables list meta-data', :issue, :issues
it_behaves_like 'issuables requiring filter', :issues
it 'includes tasks in issue list' do
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index e73e115b77d..68ae1ca218b 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -122,6 +122,59 @@ RSpec.describe Explore::ProjectsController, feature_category: :groups_and_projec
end
end
end
+
+ describe 'GET #topic.atom' do
+ context 'when topic does not exist' do
+ it 'renders a 404 error' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when topic exists' do
+ let(:topic) { create(:topic, name: 'topic1') }
+ let_it_be(:older_project) { create(:project, :public, updated_at: 1.day.ago) }
+ let_it_be(:newer_project) { create(:project, :public, updated_at: 2.days.ago) }
+
+ before do
+ create(:project_topic, project: older_project, topic: topic)
+ create(:project_topic, project: newer_project, topic: topic)
+ end
+
+ it 'renders the template' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('topic', layout: :xml)
+ end
+
+ it 'sorts repos by descending creation date' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(assigns(:projects)).to match_array [newer_project, older_project]
+ end
+
+ it 'finds topic by case insensitive name' do
+ get :topic, format: :atom, params: { topic_name: 'TOPIC1' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('topic', layout: :xml)
+ end
+
+ describe 'when topic contains more than 20 projects' do
+ before do
+ create_list(:project, 22, :public, topics: [topic])
+ end
+
+ it 'does not assigns more than 20 projects' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(assigns(:projects).count).to be(20)
+ end
+ end
+ end
+ end
end
shared_examples "blocks high page numbers" do
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index b4a7e41ccd2..be47b32ec4f 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -431,6 +431,13 @@ RSpec.describe GraphqlController, feature_category: :integrations do
context 'when querying an IntrospectionQuery', :use_clean_rails_memory_store_caching do
let_it_be(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) }
+ it 'caches IntrospectionQuery even when operationName is not given' do
+ expect(GitlabSchema).to receive(:execute).exactly(:once)
+
+ post :execute, params: { query: query }
+ post :execute, params: { query: query }
+ end
+
it 'caches the IntrospectionQuery' do
expect(GitlabSchema).to receive(:execute).exactly(:once)
@@ -461,35 +468,9 @@ RSpec.describe GraphqlController, feature_category: :integrations do
post :execute, params: { query: query, operationName: 'IntrospectionQuery' }
end
- it 'logs that it will try to hit the cache' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: "IntrospectionQueryCache hit")
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "IntrospectionQueryCache",
- can_use_introspection_query_cache: "true",
- query: query.to_s,
- variables: "{}",
- introspection_query_cache_key: "[\"introspection-query-cache\", \"#{Gitlab.revision}\", false]"
- )
-
- post :execute, params: { query: query, operationName: 'IntrospectionQuery' }
- end
-
context 'when there is an unknown introspection query' do
let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/fake_introspection.graphql')) }
- it 'logs that it did not try to hit the cache' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: "IntrospectionQueryCache miss")
- expect(Gitlab::AppLogger).to receive(:info).with(
- message: "IntrospectionQueryCache",
- can_use_introspection_query_cache: "false",
- query: query.to_s,
- variables: "{}",
- introspection_query_cache_key: "[\"introspection-query-cache\", \"#{Gitlab.revision}\", false]"
- )
-
- post :execute, params: { query: query, operationName: 'IntrospectionQuery' }
- end
-
it 'does not cache an unknown introspection query' do
expect(GitlabSchema).to receive(:execute).exactly(:twice)
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index ee8b2dce298..82dd8c18cfd 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -222,13 +222,13 @@ RSpec.describe Groups::ChildrenController, feature_category: :groups_and_project
control = ActiveRecord::QueryRecorder.new { get_list }
_new_project = create(:project, :public, namespace: group)
- expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project)
+ expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project + 1)
end
context 'when rendering hierarchies' do
# When loading hierarchies we load the all the ancestors for matched projects
- # in 2 separate queries
- let(:extra_queries_for_hierarchies) { 2 }
+ # in 3 separate queries
+ let(:extra_queries_for_hierarchies) { 3 }
def get_filtered_list
get :index, params: { group_id: group.to_param, filter: 'filter' }, format: :json
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index fe4b80e12fe..feebdd972aa 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -234,7 +234,7 @@ RSpec.describe Groups::GroupMembersController do
it 'returns correct json response' do
expect(json_response).to eq({
"expires_soon" => false,
- "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
+ "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_fs(:medium)
})
end
end
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 9ae5cb6f87c..37242bce6bf 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -65,52 +65,28 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
end
describe '#new' do
- context 'when create_runner_workflow_for_namespace is enabled' do
+ context 'when user is owner' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: [group])
- end
-
- context 'when user is owner' do
- before do
- group.add_owner(user)
- end
-
- it 'renders new with 200 status code' do
- get :new, params: { group_id: group }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:new)
- end
+ group.add_owner(user)
end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
- end
-
- it 'renders a 404' do
- get :new, params: { group_id: group }
+ it 'renders new with 200 status code' do
+ get :new, params: { group_id: group }
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:new)
end
end
- context 'when create_runner_workflow_for_namespace is disabled' do
+ context 'when user is not owner' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
+ group.add_maintainer(user)
end
- context 'when user is owner' do
- before do
- group.add_owner(user)
- end
-
- it 'renders a 404' do
- get :new, params: { group_id: group }
+ it 'renders a 404' do
+ get :new, params: { group_id: group }
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -118,66 +94,40 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
describe '#register' do
subject(:register) { get :register, params: { group_id: group, id: new_runner } }
- context 'when create_runner_workflow_for_namespace is enabled' do
+ context 'when user is owner' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: [group])
+ group.add_owner(user)
end
- context 'when user is owner' do
- before do
- group.add_owner(user)
- end
-
- context 'when runner can be registered after creation' do
- let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
+ context 'when runner can be registered after creation' do
+ let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
- it 'renders a :register template' do
- register
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:register)
- end
- end
-
- context 'when runner cannot be registered after creation' do
- let_it_be(:new_runner) { runner }
-
- it 'returns :not_found' do
- register
+ it 'renders a :register template' do
+ register
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:register)
end
end
- context 'when user is not owner' do
- before do
- group.add_maintainer(user)
- end
-
- context 'when runner can be registered after creation' do
- let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
+ context 'when runner cannot be registered after creation' do
+ let_it_be(:new_runner) { runner }
- it 'returns :not_found' do
- register
+ it 'returns :not_found' do
+ register
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
- context 'when create_runner_workflow_for_namespace is disabled' do
- let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
-
+ context 'when user is not owner' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
+ group.add_maintainer(user)
end
- context 'when user is owner' do
- before do
- group.add_owner(user)
- end
+ context 'when runner can be registered after creation' do
+ let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
it 'returns :not_found' do
register
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
index 6649e8f057c..7795fff5541 100644
--- a/spec/controllers/groups/uploads_controller_spec.rb
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -76,31 +76,17 @@ RSpec.describe Groups::UploadsController do
context 'when uploader class does not match the upload' do
let(:uploader_class) { FileUploader }
- it 'responds with status 200 but logs a deprecation message' do
- expect(Gitlab::AppJsonLogger).to receive(:info).with(
- message: 'Deprecated usage of build_uploader_from_params',
- uploader_class: uploader_class.name,
- path: filename,
- exists: true
- )
-
+ it 'responds with status 404' do
show_upload
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when filename does not match' do
let(:invalid_filename) { 'invalid_filename.jpg' }
- it 'responds with status 404 and logs a deprecation message' do
- expect(Gitlab::AppJsonLogger).to receive(:info).with(
- message: 'Deprecated usage of build_uploader_from_params',
- uploader_class: uploader_class.name,
- path: invalid_filename,
- exists: false
- )
-
+ it 'responds with status 404' do
get :show, params: params.merge(secret: secret, filename: invalid_filename)
expect(response).to have_gitlab_http_status(:not_found)
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index 3b099ba2613..273dfd6a9c7 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Import::FogbugzController, feature_category: :importers do
end
describe 'POST #callback' do
- let(:xml_response) { %Q(<?xml version=\"1.0\" encoding=\"UTF-8\"?><response><token><![CDATA[#{token}]]></token></response>) }
+ let(:xml_response) { %(<?xml version=\"1.0\" encoding=\"UTF-8\"?><response><token><![CDATA[#{token}]]></token></response>) }
before do
stub_request(:post, "https://example.com/api.asp").to_return(status: 200, body: xml_response, headers: {})
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 3476c7b8465..4772c3f3487 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -298,4 +298,15 @@ RSpec.describe Oauth::AuthorizationsController do
it 'includes Two-factor enforcement concern' do
expect(described_class.included_modules.include?(EnforcesTwoFactorAuthentication)).to eq(true)
end
+
+ describe 'Gon variables' do
+ it 'adds Gon variables' do
+ expect(controller).to receive(:add_gon_variables)
+ get :new, params: params
+ end
+
+ it 'includes GonHelper module' do
+ expect(controller).to be_a(Gitlab::GonHelper)
+ end
+ end
end
diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb
deleted file mode 100644
index d80147b5c59..00000000000
--- a/spec/controllers/projects/alert_management_controller_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::AlertManagementController do
- let_it_be(:project) { create(:project) }
- let_it_be(:role) { :developer }
- let_it_be(:user) { create(:user) }
- let_it_be(:id) { 1 }
-
- before do
- project.add_role(user, role)
- sign_in(user)
- end
-
- describe 'GET #index' do
- it 'shows the page' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'when user is unauthorized' do
- let(:role) { :reporter }
-
- it 'shows 404' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET #details' do
- it 'shows the page' do
- get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'when user is unauthorized' do
- let(:role) { :reporter }
-
- it 'shows 404' do
- get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'set_alert_id' do
- it 'sets alert id from the route' do
- get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
-
- expect(assigns(:alert_id)).to eq(id.to_s)
- end
- end
-end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index c7b74b5cf68..44615506e5d 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
download_artifact
- expect(response.headers['Content-Disposition']).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ expect(response.headers['Content-Disposition']).to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
@@ -135,7 +135,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
download_artifact(file_type: 'archive')
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Content-Disposition']).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ expect(response.headers['Content-Disposition']).to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
end
@@ -168,7 +168,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
download_artifact(file_type: file_type)
- expect(response.headers['Content-Disposition']).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ expect(response.headers['Content-Disposition']).to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
@@ -264,7 +264,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Disposition'])
- .to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ .to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
end
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
index 70178083e71..d0bfbeae78f 100644
--- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -131,6 +131,10 @@ RSpec.describe Projects::AutocompleteSourcesController do
end
shared_examples 'all members are returned' do
+ before do
+ stub_feature_flags(disable_all_mention: false)
+ end
+
it 'returns an array of member object' do
get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
@@ -155,6 +159,19 @@ RSpec.describe Projects::AutocompleteSourcesController do
name: invited_private_member.name,
avatar_url: invited_private_member.avatar_url)
end
+
+ context 'when `disable_all_mention` FF is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+ end
+
+ it 'does not return the all mention user' do
+ get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
+
+ expect(json_response).not_to include(a_hash_including(
+ { username: 'all', name: 'All Project and Group Members' }))
+ end
+ end
end
context 'with issue' do
@@ -180,6 +197,10 @@ RSpec.describe Projects::AutocompleteSourcesController do
end
shared_examples 'only public members are returned for public project' do
+ before do
+ stub_feature_flags(disable_all_mention: false)
+ end
+
it 'only returns public members' do
get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
@@ -193,6 +214,19 @@ RSpec.describe Projects::AutocompleteSourcesController do
name: user.name,
avatar_url: user.avatar_url)
end
+
+ context 'when `disable_all_mention` FF is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+ end
+
+ it 'does not return the all mention user' do
+ get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
+
+ expect(json_response).not_to include(a_hash_including(
+ { username: 'all', name: 'All Project and Group Members' }))
+ end
+ end
end
context 'with issue' do
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index b07cb7a228d..49c1935c4a3 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -5,15 +5,21 @@ require 'spec_helper'
RSpec.describe Projects::BlobController, feature_category: :source_code_management do
include ProjectForksHelper
- let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
- let(:previous_default_branch) { nil }
+ let_it_be(:project) { create(:project, :public, :repository) }
describe "GET show" do
- let(:params) { { namespace_id: project.namespace, project_id: project, id: id } }
+ let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
+ let(:ref_type) { nil }
let(:request) do
get(:show, params: params)
end
+ let(:redirect_with_ref_type) { true }
+
+ before do
+ stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
+ end
+
render_views
context 'with file path' do
@@ -24,25 +30,43 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
request
end
+ after do
+ project.repository.rm_tag(project.creator, 'ambiguous_ref')
+ project.repository.rm_branch(project.creator, 'ambiguous_ref')
+ end
+
context 'when the ref is ambiguous' do
let(:ref) { 'ambiguous_ref' }
let(:path) { 'README.md' }
let(:id) { "#{ref}/#{path}" }
- let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
- context 'and explicitly requesting a branch' do
- let(:ref_type) { 'heads' }
+ context 'and the redirect_with_ref_type flag is disabled' do
+ let(:redirect_with_ref_type) { false }
+
+ context 'and explicitly requesting a branch' do
+ let(:ref_type) { 'heads' }
+
+ it 'redirects to blob#show with sha for the branch' do
+ expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}"))
+ end
+ end
+
+ context 'and explicitly requesting a tag' do
+ let(:ref_type) { 'tags' }
- it 'redirects to blob#show with sha for the branch' do
- expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}"))
+ it 'responds with success' do
+ expect(response).to be_ok
+ end
end
end
- context 'and explicitly requesting a tag' do
- let(:ref_type) { 'tags' }
+ context 'and the redirect_with_ref_type flag is enabled' do
+ context 'when the ref_type is nil' do
+ let(:ref_type) { nil }
- it 'responds with success' do
- expect(response).to be_ok
+ it 'redirects to the tag' do
+ expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags'))
+ end
end
end
end
@@ -68,18 +92,20 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
it { is_expected.to respond_with(:not_found) }
end
- context "renamed default branch, valid file" do
- let(:id) { 'old-default-branch/README.md' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context 'when default branch was renamed' do
+ let_it_be_with_reload(:project) { create(:project, :public, :repository, previous_default_branch: 'old-default-branch') }
- it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") }
- end
+ context "renamed default branch, valid file" do
+ let(:id) { 'old-default-branch/README.md' }
+
+ it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") }
+ end
- context "renamed default branch, invalid file" do
- let(:id) { 'old-default-branch/invalid-path.rb' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context "renamed default branch, invalid file" do
+ let(:id) { 'old-default-branch/invalid-path.rb' }
- it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") }
+ it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") }
+ end
end
context "binary file" do
@@ -100,7 +126,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
let(:id) { 'master/README.md' }
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: id }, format: :json
+ get :show, params: params, format: :json
end
it do
@@ -114,7 +140,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
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, ref_type: 'heads', viewer: 'none' }, format: :json
end
it do
@@ -127,7 +153,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
context 'with tree path' do
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
+ get :show, params: params
controller.instance_variable_set(:@blob, nil)
end
@@ -414,6 +440,10 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme
let(:after_delete_path) { project_tree_path(project, 'master/files') }
it 'redirects to the sub directory' do
+ expect_next_instance_of(Files::DeleteService) do |instance|
+ expect(instance).to receive(:execute).and_return({ status: :success })
+ end
+
delete :destroy, params: default_params
expect(response).to redirect_to(after_delete_path)
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index bface886674..15b7ddd85ea 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -113,18 +113,6 @@ RSpec.describe Projects::ClustersController, feature_category: :deployment_manag
end
end
- it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- let(:metrics_dashboard_req_params) do
- {
- id: cluster.id,
- namespace_id: project.namespace.full_path,
- project_id: project.path
- }
- end
- end
-
describe 'POST create for existing cluster' do
let(:params) do
{
diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
index a7f3212a6f9..eaef455837f 100644
--- a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
+++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
@@ -129,7 +129,7 @@ RSpec.describe Projects::DesignManagement::Designs::RawImagesController do
it 'serves files with `Content-Disposition: attachment`' do
subject
- expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
+ expect(response.header['Content-Disposition']).to eq(%(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
end
it 'sets appropriate caching headers' do
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 1bb5112681c..b4667b4568f 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
@@ -51,7 +51,7 @@ RSpec.describe Projects::DesignManagement::Designs::ResizedImageController, feat
it 'sets Content-Disposition as attachment' do
filename = design.filename
- expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
+ expect(response.header['Content-Disposition']).to eq(%(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
end
it 'serves files with Workhorse' do
@@ -59,7 +59,7 @@ RSpec.describe Projects::DesignManagement::Designs::ResizedImageController, feat
end
it 'sets appropriate caching headers' do
- expect(response.header['Cache-Control']).to eq('private')
+ expect(response.header['Cache-Control']).to eq('max-age=0, private, must-revalidate')
expect(response.header['ETag']).to be_present
end
end
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
deleted file mode 100644
index 9bc4a83030e..00000000000
--- a/spec/controllers/projects/grafana_api_controller_spec.rb
+++ /dev/null
@@ -1,268 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::GrafanaApiController, feature_category: :metrics do
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let(:anonymous) { nil }
- let(:user) { reporter }
-
- before_all do
- project.add_reporter(reporter)
- project.add_guest(guest)
- end
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- sign_in(user) if user
- end
-
- describe 'GET #proxy' do
- let(:proxy_service) { instance_double(Grafana::ProxyService) }
- let(:params) do
- {
- namespace_id: project.namespace.full_path,
- project_id: project.path,
- proxy_path: 'api/v1/query_range',
- datasource_id: '1',
- query: 'rate(relevant_metric)',
- start_time: '1570441248',
- end_time: '1570444848',
- step: '900'
- }
- end
-
- before do
- allow(Grafana::ProxyService).to receive(:new).and_return(proxy_service)
- allow(proxy_service).to receive(:execute).and_return(service_result)
- end
-
- shared_examples_for 'error response' do |http_status|
- it "returns #{http_status}" do
- get :proxy, params: params
-
- expect(response).to have_gitlab_http_status(http_status)
- expect(json_response['status']).to eq('error')
- expect(json_response['message']).to eq('error message')
- end
- end
-
- shared_examples_for 'accessible' do
- let(:service_result) { nil }
-
- it 'returns non erroneous response' do
- get :proxy, params: params
-
- # We don't care about the specific code as long it's not an error.
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
-
- shared_examples_for 'not accessible' do
- let(:service_result) { nil }
-
- it 'returns 404 Not found' do
- get :proxy, params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(Grafana::ProxyService).not_to have_received(:new)
- end
- end
-
- shared_examples_for 'login required' do
- let(:service_result) { nil }
-
- it 'redirects to login page' do
- get :proxy, params: params
-
- expect(response).to redirect_to(new_user_session_path)
- expect(Grafana::ProxyService).not_to have_received(:new)
- end
- end
-
- context 'with a successful result' do
- let(:service_result) { { status: :success, body: '{}' } }
-
- 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(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({})
- end
- end
-
- context 'when the request is still unavailable' do
- let(:service_result) { nil }
-
- it 'returns 204 no content' do
- get :proxy, params: params
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(json_response['status']).to eq('processing')
- expect(json_response['message']).to eq('Not ready yet. Try again later.')
- end
- end
-
- context 'when an error has occurred' do
- context 'with an error accessing grafana' do
- let(:service_result) do
- {
- http_status: :service_unavailable,
- status: :error,
- message: 'error message'
- }
- end
-
- it_behaves_like 'error response', :service_unavailable
- end
-
- context 'with a processing error' do
- let(:service_result) do
- {
- status: :error,
- message: 'error message'
- }
- end
-
- it_behaves_like 'error response', :bad_request
- end
- end
-
- context 'as guest' do
- let(:user) { guest }
-
- it_behaves_like 'not accessible'
- end
-
- context 'as anonymous' do
- let(:user) { anonymous }
-
- it_behaves_like 'not accessible'
- end
-
- context 'on a private project' do
- let_it_be(:project) { create(:project, :private) }
-
- before_all do
- project.add_guest(guest)
- end
-
- context 'as anonymous' do
- let(:user) { anonymous }
-
- it_behaves_like 'login required'
- end
-
- context 'as guest' do
- let(:user) { guest }
-
- it_behaves_like 'accessible'
- end
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it_behaves_like 'not accessible'
- end
- end
-
- describe 'GET #metrics_dashboard' do
- let(:service_result) { { status: :success, dashboard: '{}' } }
- let(:params) do
- {
- format: :json,
- embedded: true,
- grafana_url: 'https://grafana.example.com',
- namespace_id: project.namespace.full_path,
- project_id: project.path
- }
- end
-
- before do
- allow(Gitlab::Metrics::Dashboard::Finder)
- .to receive(:find)
- .and_return(service_result)
- end
-
- context 'when the result is still processing' do
- let(:service_result) { nil }
-
- it 'returns 204 no content' do
- get :metrics_dashboard, params: params
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
-
- context 'when the result was successful' do
- it 'returns the dashboard response' do
- get :metrics_dashboard, params: params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include({
- 'dashboard' => '{}',
- 'status' => 'success'
- })
- expect(json_response).to include('metrics_data')
- end
- end
-
- context 'when an error has occurred' do
- shared_examples_for 'error response' do |http_status|
- it "returns #{http_status}" do
- get :metrics_dashboard, params: params
-
- expect(response).to have_gitlab_http_status(http_status)
- expect(json_response['status']).to eq('error')
- expect(json_response['message']).to eq('error message')
- end
- end
-
- context 'with an error accessing grafana' do
- let(:service_result) do
- {
- http_status: :service_unavailable,
- status: :error,
- message: 'error message'
- }
- end
-
- it_behaves_like 'error response', :service_unavailable
- end
-
- context 'with a processing error' do
- let(:service_result) { { status: :error, message: 'error message' } }
-
- it_behaves_like 'error response', :bad_request
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it 'returns 404 Not found' do
- get :metrics_dashboard, params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to be_empty
- end
- end
- end
- end
-end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 5e9135c00e3..f9ce77a44ba 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1809,7 +1809,7 @@ RSpec.describe Projects::IssuesController, :request_store, feature_category: :te
create(:user_status, user: second_discussion.author)
expect { get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } }
- .not_to exceed_query_limit(control)
+ .not_to exceed_query_limit(control).with_threshold(9)
end
context 'when user is setting notes filters' do
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index c3a5255b584..68fbeb00b67 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
it 'does not allow draft note creation' do
- expect { create_draft_note }.to change { DraftNote.count }.by(0)
+ expect { create_draft_note }.not_to change { DraftNote.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -172,6 +172,33 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
end
end
end
+
+ context 'when the draft note is invalid' do
+ let_it_be(:draft_note) { DraftNote.new }
+
+ before do
+ errors = ActiveModel::Errors.new(draft_note)
+ errors.add(:base, 'Error 1')
+ errors.add(:base, 'Error 2')
+
+ allow(draft_note).to receive(:errors).and_return(errors)
+
+ allow_next_instance_of(DraftNotes::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(draft_note)
+ end
+ end
+
+ it 'does not allow draft note creation' do
+ expect { create_draft_note }.not_to change { DraftNote.count }
+ end
+
+ it "returns status 422", :aggregate_failures do
+ create_draft_note
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(response.body).to eq('{"errors":"Error 1 and Error 2"}')
+ end
+ end
end
describe 'PUT #update' do
@@ -212,6 +239,30 @@ RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :cod
expect(draft.note).to eq('This is an updated unpublished comment')
expect(json_response['note_html']).not_to be_empty
end
+
+ context 'when the draft note is invalid' do
+ before do
+ errors = ActiveModel::Errors.new(draft)
+ errors.add(:base, 'Error 1')
+ errors.add(:base, 'Error 2')
+
+ allow_next_found_instance_of(DraftNote) do |instance|
+ allow(instance).to receive(:update).and_return(false)
+ allow(instance).to receive(:errors).and_return(errors)
+ end
+ end
+
+ it 'does not update the draft' do
+ expect { update_draft_note }.not_to change { draft.reload.note }
+ end
+
+ it 'returns status 422', :aggregate_failures do
+ update_draft_note
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(response.body).to eq('{"errors":"Error 1 and Error 2"}')
+ end
+ end
end
describe 'POST #publish' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 4a5283f1127..940f6fed906 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -39,6 +39,12 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
specify { expect(get(:index, params: request_params)).to have_request_urgency(:medium) }
+ it 'sets the correct feature category' do
+ get :index, params: request_params
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('team_planning')
+ end
+
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
@@ -149,6 +155,12 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
expect(note_json[:discussion_line_code]).to be_nil
end
+ it 'sets the correct feature category' do
+ get :index, params: params
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('source_code_management')
+ end
+
context 'when user cannot read commit' do
before do
allow(Ability).to receive(:allowed?).and_call_original
@@ -164,7 +176,29 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
end
end
- context 'for a regular note' do
+ context 'for a snippet note' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let!(:note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
+
+ let(:params) { request_params.merge(target_type: 'project_snippet', target_id: project_snippet.id, html: true) }
+
+ it 'responds with the expected attributes' do
+ get :index, params: params
+
+ expect(note_json[:id]).to eq(note.id)
+ expect(note_json[:discussion_html]).to be_nil
+ expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
+ end
+
+ it 'sets the correct feature category' do
+ get :index, params: params
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('source_code_management')
+ end
+ end
+
+ context 'for a merge request note' do
let!(:note) { create(:note_on_merge_request, project: project) }
let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id, html: true) }
@@ -178,6 +212,12 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
expect(note_json[:diff_discussion_html]).to be_nil
expect(note_json[:discussion_line_code]).to be_nil
end
+
+ it 'sets the correct feature category' do
+ get :index, params: params
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('code_review_workflow')
+ end
end
context 'with cross-reference system note', :request_store do
@@ -253,6 +293,68 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
create!
end
+ it 'sets the correct feature category' do
+ create!
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('code_review_workflow')
+ end
+
+ context 'on an issue' do
+ let(:request_params) do
+ {
+ note: { note: note_text, noteable_id: issue.id, noteable_type: 'Issue' },
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'issue',
+ target_id: issue.id
+ }
+ end
+
+ it 'sets the correct feature category' do
+ create!
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('team_planning')
+ end
+ end
+
+ context 'on a commit' do
+ let(:commit_id) { RepoHelpers.sample_commit.id }
+ let(:request_params) do
+ {
+ note: { note: note_text, commit_id: commit_id, noteable_type: 'Commit' },
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'commit',
+ target_id: commit_id
+ }
+ end
+
+ it 'sets the correct feature category' do
+ create!
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('source_code_management')
+ end
+ end
+
+ context 'on a project snippet' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let(:request_params) do
+ {
+ note: { note: note_text, noteable_id: project_snippet.id, noteable_type: 'ProjectSnippet' },
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'project_snippet',
+ target_id: project_snippet.id
+ }
+ end
+
+ it 'sets the correct feature category' do
+ create!
+
+ expect(::Gitlab::ApplicationContext.current_context_attribute(:feature_category)).to eq('source_code_management')
+ end
+ end
+
context 'the project is publically available' do
context 'for HTML' do
it "returns status 302" do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 6d810fdcd51..486062fe52b 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -106,7 +106,8 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
end
end
- describe 'POST #create' do
+ # Move this from `shared_context` to `describe` when `ci_refactoring_pipeline_schedule_create_service` is removed.
+ shared_context 'POST #create' do # rubocop:disable RSpec/ContextWording
describe 'functionality' do
before do
project.add_developer(user)
@@ -184,6 +185,16 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
end
end
+ it_behaves_like 'POST #create'
+
+ context 'when the FF ci_refactoring_pipeline_schedule_create_service is disabled' do
+ before do
+ stub_feature_flags(ci_refactoring_pipeline_schedule_create_service: false)
+ end
+
+ it_behaves_like 'POST #create'
+ end
+
describe 'PUT #update' do
describe 'functionality' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 8c5f8fc6259..a5542a2b825 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -328,7 +328,7 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
expect do
get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
- end.not_to exceed_all_query_limit(control)
+ end.not_to exceed_all_query_limit(control).with_threshold(3)
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index ad49529b426..9657cf33afd 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -320,7 +320,7 @@ RSpec.describe Projects::ProjectMembersController do
it 'returns correct json response' do
expect(json_response).to eq({
"expires_soon" => false,
- "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
+ "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_fs(:medium)
})
end
end
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index e0e4d0f7bc5..d6816bd49af 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -28,52 +28,28 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
}
end
- context 'when create_runner_workflow_for_namespace is enabled' do
+ context 'when user is maintainer' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
+ project.add_maintainer(user)
end
- context 'when user is maintainer' do
- before do
- project.add_maintainer(user)
- end
-
- it 'renders new with 200 status code' do
- get :new, params: params
+ it 'renders new with 200 status code' do
+ get :new, params: params
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:new)
- end
- end
-
- context 'when user is not maintainer' do
- before do
- project.add_developer(user)
- end
-
- it 'renders a 404' do
- get :new, params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:new)
end
end
- context 'when create_runner_workflow_for_namespace is disabled' do
+ context 'when user is not maintainer' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
+ project.add_developer(user)
end
- context 'when user is maintainer' do
- before do
- project.add_maintainer(user)
- end
+ it 'renders a 404' do
+ get :new, params: params
- it 'renders a 404' do
- get :new, params: params
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -81,66 +57,40 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
describe '#register' do
subject(:register) { get :register, params: { namespace_id: project.namespace, project_id: project, id: new_runner } }
- context 'when create_runner_workflow_for_namespace is enabled' do
+ context 'when user is maintainer' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
+ project.add_maintainer(user)
end
- context 'when user is maintainer' do
- before do
- project.add_maintainer(user)
- end
-
- context 'when runner can be registered after creation' do
- let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
-
- it 'renders a :register template' do
- register
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:register)
- end
- end
-
- context 'when runner cannot be registered after creation' do
- let_it_be(:new_runner) { runner }
+ context 'when runner can be registered after creation' do
+ let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
- it 'returns :not_found' do
- register
+ it 'renders a :register template' do
+ register
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:register)
end
end
- context 'when user is not maintainer' do
- before do
- project.add_developer(user)
- end
-
- context 'when runner can be registered after creation' do
- let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
+ context 'when runner cannot be registered after creation' do
+ let_it_be(:new_runner) { runner }
- it 'returns :not_found' do
- register
+ it 'returns :not_found' do
+ register
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
- context 'when create_runner_workflow_for_namespace is disabled' do
- let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
-
+ context 'when user is not maintainer' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
+ project.add_developer(user)
end
- context 'when user is maintainer' do
- before do
- project.add_maintainer(user)
- end
+ context 'when runner can be registered after creation' do
+ let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
it 'returns :not_found' do
register
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index 1c332eadc42..a1dbd27f49a 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -65,9 +65,8 @@ RSpec.describe Projects::Settings::CiCdController, feature_category: :continuous
end
context 'with group runners' do
- let_it_be(:group) { create :group }
- let_it_be(:project) { create :project, group: group }
- let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let(:project) { other_project }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
it 'sets group runners' do
subject
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 61998d516e8..ffec670e97d 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Projects::TreeController, feature_category: :source_code_management do
- let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
- let(:previous_default_branch) { nil }
+ let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:redirect_with_ref_type) { true }
before do
sign_in(user)
@@ -17,10 +17,14 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
describe "GET show" do
let(:params) do
{
- namespace_id: project.namespace.to_param, project_id: project, id: id
+ namespace_id: project.namespace.to_param, project_id: project, id: id, ref_type: ref_type
}
end
+ let(:request) { get :show, params: params }
+
+ let(:ref_type) { nil }
+
# Make sure any errors accessing the tree in our views bubble up to this spec
render_views
@@ -28,26 +32,79 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id)
project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id)
- get :show, params: params
+
+ stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type)
+ end
+
+ after do
+ project.repository.rm_tag(project.creator, 'ambiguous_ref')
+ project.repository.rm_branch(project.creator, 'ambiguous_ref')
end
- context 'when the ref is ambiguous' do
- let(:id) { 'ambiguous_ref' }
- let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
+ context 'when the redirect_with_ref_type flag is disabled' do
+ let(:redirect_with_ref_type) { false }
- context 'and explicitly requesting a branch' do
- let(:ref_type) { 'heads' }
+ context 'when there is a ref and tag with the same name' do
+ let(:id) { 'ambiguous_ref' }
+ let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
- it 'redirects to blob#show with sha for the branch' do
- expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id))
+ context 'and explicitly requesting a branch' do
+ let(:ref_type) { 'heads' }
+
+ it 'redirects to blob#show with sha for the branch' do
+ request
+ expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id))
+ end
+ end
+
+ context 'and explicitly requesting a tag' do
+ let(:ref_type) { 'tags' }
+
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
end
+ end
- context 'and explicitly requesting a tag' do
- let(:ref_type) { 'tags' }
+ describe 'delegating to ExtractsRef::RequestedRef' do
+ context 'when there is a ref and tag with the same name' do
+ let(:id) { 'ambiguous_ref' }
+ let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } }
- it 'responds with success' do
- expect(response).to be_ok
+ let(:requested_ref_double) { ExtractsRef::RequestedRef.new(project.repository, ref_type: ref_type, ref: id) }
+
+ before do
+ allow(ExtractsRef::RequestedRef).to receive(:new).with(kind_of(Repository), ref_type: ref_type, ref: id).and_return(requested_ref_double)
+ end
+
+ context 'and not specifying a ref_type' do
+ it 'finds the tags and redirects' do
+ expect(requested_ref_double).to receive(:find).and_call_original
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{id}/?ref_type=tags")
+ end
+ end
+
+ context 'and explicitly requesting a branch' do
+ let(:ref_type) { 'heads' }
+
+ it 'finds the branch' do
+ expect(requested_ref_double).not_to receive(:find)
+ request
+ expect(response).to be_ok
+ end
+ end
+
+ context 'and explicitly requesting a tag' do
+ let(:ref_type) { 'tags' }
+
+ it 'finds the tag' do
+ expect(requested_ref_double).not_to receive(:find)
+ request
+ expect(response).to be_ok
+ end
end
end
end
@@ -55,19 +112,26 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
context "valid branch, no path" do
let(:id) { 'master' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
context "valid branch, valid path" do
let(:id) { 'master/encoding/' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
context "valid branch, invalid path" do
let(:id) { 'master/invalid-path/' }
it 'redirects' do
+ request
expect(subject)
.to redirect_to("/#{project.full_path}/-/tree/master")
end
@@ -76,54 +140,91 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
context "invalid branch, valid path" do
let(:id) { 'invalid-branch/encoding/' }
- it { is_expected.to respond_with(:not_found) }
+ it 'responds with not_found' do
+ request
+ expect(subject).to respond_with(:not_found)
+ end
end
- context "renamed default branch, valid file" do
- let(:id) { 'old-default-branch/encoding/' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context 'when default branch was renamed' do
+ let_it_be_with_reload(:project) { create(:project, :repository, previous_default_branch: 'old-default-branch') }
- it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/") }
- end
+ context "and the file is valid" do
+ let(:id) { 'old-default-branch/encoding/' }
+
+ it 'redirects' do
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/")
+ end
+ end
- context "renamed default branch, invalid file" do
- let(:id) { 'old-default-branch/invalid-path/' }
- let(:previous_default_branch) { 'old-default-branch' }
+ context "and the file is invalid" do
+ let(:id) { 'old-default-branch/invalid-path/' }
- it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/") }
+ it 'redirects' do
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/")
+ end
+ end
end
context "valid empty branch, invalid path" do
let(:id) { 'empty-branch/invalid-path/' }
it 'redirects' do
- expect(subject)
- .to redirect_to("/#{project.full_path}/-/tree/empty-branch")
+ request
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/empty-branch")
end
end
context "valid empty branch" do
let(:id) { 'empty-branch' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
end
context "invalid SHA commit ID" do
let(:id) { 'ff39438/.gitignore' }
- it { is_expected.to respond_with(:not_found) }
+ it 'responds with not_found' do
+ request
+ expect(subject).to respond_with(:not_found)
+ end
end
context "valid SHA commit ID" do
let(:id) { '6d39438' }
- it { is_expected.to respond_with(:success) }
+ it 'responds with success' do
+ request
+ expect(response).to be_ok
+ end
+
+ context 'and there is a tag with the same name' do
+ before do
+ project.repository.add_tag(project.creator, id, RepoHelpers.sample_commit.id)
+ end
+
+ it 'responds with success' do
+ request
+
+ # This uses the tag
+ # TODO: Should we redirect in this case?
+ expect(response).to be_ok
+ end
+ end
end
context "valid SHA commit ID with path" do
let(:id) { '6d39438/.gitignore' }
- it { expect(response).to have_gitlab_http_status(:found) }
+ it 'responds with found' do
+ request
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
end
@@ -149,7 +250,7 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
before do
get :show, params: {
- namespace_id: project.namespace.to_param, project_id: project, id: id
+ namespace_id: project.namespace.to_param, project_id: project, id: id, ref_type: 'heads'
}
end
@@ -157,7 +258,7 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme
let(:id) { 'master/README.md' }
it 'redirects' do
- redirect_url = "/#{project.full_path}/-/blob/master/README.md"
+ redirect_url = "/#{project.full_path}/-/blob/master/README.md?ref_type=heads"
expect(subject).to redirect_to(redirect_url)
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 6adddccfda7..46913cfa649 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -164,107 +164,113 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
end
end
- context 'when there is a tag with the same name as the default branch' do
- let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) }
- let(:tree_with_default_branch) do
- branch = tagged_project.repository.find_branch(tagged_project.default_branch)
- project_tree_path(tagged_project, branch.target)
- end
-
+ context 'when redirect_with_ref_type is disabled' do
before do
- tagged_project.repository.create_file(
- tagged_project.creator,
- 'file_for_tag',
- 'content for file',
- message: "Automatically created file",
- branch_name: 'branch-to-tag'
- )
-
- tagged_project.repository.add_tag(
- tagged_project.creator,
- tagged_project.default_branch, # tag name
- 'branch-to-tag' # target
- )
- end
-
- it 'redirects to tree view for the default branch' do
- get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project }
- expect(response).to redirect_to(tree_with_default_branch)
- end
- end
-
- context 'when the default branch name is ambiguous' do
- let_it_be(:project_with_default_branch) do
- create(:project, :public, :custom_repo, files: ['somefile'])
+ stub_feature_flags(redirect_with_ref_type: false)
end
- shared_examples 'ambiguous ref redirects' do
- let(:project) { project_with_default_branch }
- let(:branch_ref) { "refs/heads/#{ref}" }
- let(:repo) { project.repository }
+ context 'when there is a tag with the same name as the default branch' do
+ let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) }
+ let(:tree_with_default_branch) do
+ branch = tagged_project.repository.find_branch(tagged_project.default_branch)
+ project_tree_path(tagged_project, branch.target)
+ end
before do
- repo.create_branch(branch_ref, 'master')
- repo.change_head(ref)
+ tagged_project.repository.create_file(
+ tagged_project.creator,
+ 'file_for_tag',
+ 'content for file',
+ message: "Automatically created file",
+ branch_name: 'branch-to-tag'
+ )
+
+ tagged_project.repository.add_tag(
+ tagged_project.creator,
+ tagged_project.default_branch, # tag name
+ 'branch-to-tag' # target
+ )
end
- after do
- repo.change_head('master')
- repo.delete_branch(branch_ref)
+ it 'redirects to tree view for the default branch' do
+ get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project }
+ expect(response).to redirect_to(tree_with_default_branch)
end
+ end
- subject do
- get(
- :show,
- params: {
- namespace_id: project.namespace,
- id: project
- }
- )
+ context 'when the default branch name is ambiguous' do
+ let_it_be(:project_with_default_branch) do
+ create(:project, :public, :custom_repo, files: ['somefile'])
end
- context 'when there is no conflicting ref' do
- let(:other_ref) { 'non-existent-ref' }
+ shared_examples 'ambiguous ref redirects' do
+ let(:project) { project_with_default_branch }
+ let(:branch_ref) { "refs/heads/#{ref}" }
+ let(:repo) { project.repository }
- it { is_expected.to have_gitlab_http_status(:ok) }
- end
+ before do
+ repo.create_branch(branch_ref, 'master')
+ repo.change_head(ref)
+ end
+
+ after do
+ repo.change_head('master')
+ repo.delete_branch(branch_ref)
+ end
- context 'and that other ref exists' do
- let(:other_ref) { 'master' }
+ subject do
+ get(
+ :show,
+ params: {
+ namespace_id: project.namespace,
+ id: project
+ }
+ )
+ end
+
+ context 'when there is no conflicting ref' do
+ let(:other_ref) { 'non-existent-ref' }
- let(:project_default_root_tree_path) do
- sha = repo.find_branch(project.default_branch).target
- project_tree_path(project, sha)
+ it { is_expected.to have_gitlab_http_status(:ok) }
end
- it 'redirects to tree view for the default branch' do
- is_expected.to redirect_to(project_default_root_tree_path)
+ context 'and that other ref exists' do
+ let(:other_ref) { 'master' }
+
+ let(:project_default_root_tree_path) do
+ sha = repo.find_branch(project.default_branch).target
+ project_tree_path(project, sha)
+ end
+
+ it 'redirects to tree view for the default branch' do
+ is_expected.to redirect_to(project_default_root_tree_path)
+ end
end
end
- end
- context 'when ref starts with ref/heads/' do
- let(:ref) { "refs/heads/#{other_ref}" }
+ context 'when ref starts with ref/heads/' do
+ let(:ref) { "refs/heads/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
- end
+ include_examples 'ambiguous ref redirects'
+ end
- context 'when ref starts with ref/tags/' do
- let(:ref) { "refs/tags/#{other_ref}" }
+ context 'when ref starts with ref/tags/' do
+ let(:ref) { "refs/tags/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
- end
+ include_examples 'ambiguous ref redirects'
+ end
- context 'when ref starts with heads/' do
- let(:ref) { "heads/#{other_ref}" }
+ context 'when ref starts with heads/' do
+ let(:ref) { "heads/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
- end
+ include_examples 'ambiguous ref redirects'
+ end
- context 'when ref starts with tags/' do
- let(:ref) { "tags/#{other_ref}" }
+ context 'when ref starts with tags/' do
+ let(:ref) { "tags/#{other_ref}" }
- include_examples 'ambiguous ref redirects'
+ include_examples 'ambiguous ref redirects'
+ end
end
end
end
diff --git a/spec/controllers/registrations/welcome_controller_spec.rb b/spec/controllers/registrations/welcome_controller_spec.rb
index 4118754144c..5a3feefc1ba 100644
--- a/spec/controllers/registrations/welcome_controller_spec.rb
+++ b/spec/controllers/registrations/welcome_controller_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe Registrations::WelcomeController, feature_category: :system_acces
end
context 'when the new user already has more than 1 accepted group membership' do
- it 'redirects to the most recent membership group activty page' do
+ it 'redirects to the most recent membership group activity page' do
member2 = create(:group_member, user: user)
expect(subject).to redirect_to(activity_group_path(member2.source))
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 276bd9b65b9..88af7d1fe45 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
context 'when repository container is a project' do
- it_behaves_like Repositories::GitHttpController do
+ it_behaves_like described_class do
let(:container) { project }
let(:user) { project.first_owner }
let(:access_checker_class) { Gitlab::GitAccess }
@@ -133,7 +133,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
context 'when the user is a deploy token' do
- it_behaves_like Repositories::GitHttpController do
+ it_behaves_like described_class do
let(:container) { project }
let(:user) { create(:deploy_token, :project, projects: [project]) }
let(:access_checker_class) { Gitlab::GitAccess }
@@ -144,7 +144,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
context 'when repository container is a project wiki' do
- it_behaves_like Repositories::GitHttpController do
+ it_behaves_like described_class do
let(:container) { create(:project_wiki, :empty_repo, project: project) }
let(:user) { project.first_owner }
let(:access_checker_class) { Gitlab::GitAccessWiki }
@@ -155,7 +155,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
context 'when repository container is a personal snippet' do
- it_behaves_like Repositories::GitHttpController do
+ it_behaves_like described_class do
let(:container) { personal_snippet }
let(:user) { personal_snippet.author }
let(:access_checker_class) { Gitlab::GitAccessSnippet }
@@ -167,7 +167,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
context 'when repository container is a project snippet' do
- it_behaves_like Repositories::GitHttpController do
+ it_behaves_like described_class do
let(:container) { project_snippet }
let(:user) { project_snippet.author }
let(:access_checker_class) { Gitlab::GitAccessSnippet }
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 4ec6d3ad4f5..c22292cb82c 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -124,7 +124,7 @@ RSpec.describe 'Database schema', feature_category: :database do
}.with_indifferent_access.freeze
context 'for table' do
- Gitlab::Database::EachDatabase.each_database_connection do |connection, _|
+ Gitlab::Database::EachDatabase.each_connection do |connection, _|
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
(connection.tables - TABLE_PARTITIONS).sort.each do |table|
table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
@@ -244,6 +244,7 @@ RSpec.describe 'Database schema', feature_category: :database do
"GeoNodeStatus" => %w[status],
"Operations::FeatureFlagScope" => %w[strategies],
"Operations::FeatureFlags::Strategy" => %w[parameters],
+ "Organizations::OrganizationSetting" => %w[settings], # Custom validations
"Packages::Composer::Metadatum" => %w[composer_json],
"RawUsageData" => %w[payload], # Usage data payload changes often, we cannot use one schema
"Releases::Evidence" => %w[summary],
@@ -299,7 +300,7 @@ RSpec.describe 'Database schema', feature_category: :database do
context 'primary keys' do
it 'expects every table to have a primary key defined' do
- Gitlab::Database::EachDatabase.each_database_connection do |connection, _|
+ Gitlab::Database::EachDatabase.each_connection do |connection, _|
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
problematic_tables = connection.tables.select do |table|
diff --git a/spec/deprecation_warnings.rb b/spec/deprecation_warnings.rb
index 45fed5fecca..abdd13ee8e7 100644
--- a/spec/deprecation_warnings.rb
+++ b/spec/deprecation_warnings.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative '../lib/gitlab/utils'
+require 'gitlab/utils/all'
return if Gitlab::Utils.to_boolean(ENV['SILENCE_DEPRECATIONS'], default: false)
# Enable deprecation warnings by default and make them more visible
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index ef8f8cbce3b..461a6390a33 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe ApplicationExperiment, :experiment, feature_category: :experiment
# _published_experiments.html.haml partial.
application_experiment.publish
- expect(ApplicationExperiment.published_experiments['namespaced/stub']).to include(
+ expect(described_class.published_experiments['namespaced/stub']).to include(
experiment: 'namespaced/stub',
excluded: false,
key: anything,
diff --git a/spec/experiments/concerns/project_commit_count_spec.rb b/spec/experiments/concerns/project_commit_count_spec.rb
deleted file mode 100644
index f5969ad6241..00000000000
--- a/spec/experiments/concerns/project_commit_count_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProjectCommitCount do
- let(:klass) { Class.include(ProjectCommitCount) }
- let(:instance) { klass.new }
-
- describe '#commit_count_for' do
- subject { instance.commit_count_for(project, default_count: 42, caller_info: :identifiable) }
-
- let(:project) { create(:project, :repository) }
-
- context 'when a root_ref exists' do
- it 'returns commit count from GitlayClient' do
- allow(Gitlab::GitalyClient).to receive(:call).and_call_original
- allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything)
- .and_return(double(count: 4))
-
- expect(subject).to eq(4)
- end
- end
-
- context 'when a root_ref does not exist' do
- let(:project) { create(:project, :empty_repo) }
-
- it 'returns the default_count' do
- expect(subject).to eq(42)
- end
- end
-
- it "handles exceptions by logging them with exception_details and returns the default_count" do
- allow(Gitlab::GitalyClient).to receive(:call).and_call_original
- allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything).and_raise(e = StandardError.new('_message_'))
-
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, { caller_info: :identifiable })
-
- expect(subject).to eq(42)
- end
- end
-end
diff --git a/spec/experiments/force_company_trial_experiment_spec.rb b/spec/experiments/force_company_trial_experiment_spec.rb
deleted file mode 100644
index 42a3245771a..00000000000
--- a/spec/experiments/force_company_trial_experiment_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ForceCompanyTrialExperiment, :experiment do
- subject { described_class.new(current_user: user) }
-
- let(:user) { create(:user, setup_for_company: setup_for_company) }
- let(:setup_for_company) { true }
-
- context 'when a user is setup_for_company' do
- it 'is not excluded' do
- expect(subject).not_to exclude(user: user)
- end
- end
-
- context 'when a user is not setup_for_company' do
- let(:setup_for_company) { nil }
-
- it 'is excluded' do
- expect(subject).to exclude(user: user)
- end
- end
-end
diff --git a/spec/factories/ai/service_access_tokens.rb b/spec/factories/ai/service_access_tokens.rb
new file mode 100644
index 00000000000..61abf4e1144
--- /dev/null
+++ b/spec/factories/ai/service_access_tokens.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :service_access_token, class: 'Ai::ServiceAccessToken' do
+ token { SecureRandom.alphanumeric(10) }
+ expires_at { Time.current + 1.day }
+ category { :code_suggestions }
+
+ trait :active do
+ expires_at { Time.current + 1.day }
+ end
+
+ trait :expired do
+ expires_at { Time.current - 1.day }
+ end
+
+ trait :code_suggestions do
+ category { :code_suggestions }
+ end
+ end
+end
diff --git a/spec/factories/alert_management/http_integrations.rb b/spec/factories/alert_management/http_integrations.rb
index 43cf8b3c6db..1a46215de47 100644
--- a/spec/factories/alert_management/http_integrations.rb
+++ b/spec/factories/alert_management/http_integrations.rb
@@ -19,12 +19,12 @@ FactoryBot.define do
endpoint_identifier { 'legacy' }
end
- trait :prometheus do
- type_identifier { :prometheus }
- end
-
initialize_with { new(**attributes) }
- factory :alert_management_prometheus_integration, traits: [:prometheus]
+ factory :alert_management_prometheus_integration, traits: [:prometheus] do
+ trait :legacy do
+ endpoint_identifier { 'legacy-prometheus' }
+ end
+ end
end
end
diff --git a/spec/factories/audit_events.rb b/spec/factories/audit_events.rb
index 10f60591922..ceb7516441f 100644
--- a/spec/factories/audit_events.rb
+++ b/spec/factories/audit_events.rb
@@ -88,6 +88,29 @@ FactoryBot.define do
end
end
+ trait :instance_event do
+ transient { instance_scope { Gitlab::Audit::InstanceScope.new } }
+
+ entity_type { Gitlab::Audit::InstanceScope.name }
+ entity_id { instance_scope.id }
+ entity_path { instance_scope.full_path }
+ target_details { instance_scope.name }
+ ip_address { IPAddr.new '127.0.0.1' }
+ details do
+ {
+ change: 'project_creation_level',
+ from: nil,
+ to: 'Developers + Maintainers',
+ author_name: user.name,
+ target_id: instance_scope.id,
+ target_type: Gitlab::Audit::InstanceScope.name,
+ target_details: instance_scope.name,
+ ip_address: '127.0.0.1',
+ entity_path: instance_scope.full_path
+ }
+ end
+ end
+
factory :project_audit_event, traits: [:project_event]
factory :group_audit_event, traits: [:group_event]
end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index 9ea8f3ad06f..92eb67c02b4 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -28,6 +28,7 @@ FactoryBot.define do
end
after(:create) do |board|
+ board.lists.create!(list_type: :backlog)
board.lists.create!(list_type: :closed)
end
end
diff --git a/spec/factories/bulk_import/trackers.rb b/spec/factories/bulk_import/trackers.rb
index 3e69ab26801..3d5d88954ed 100644
--- a/spec/factories/bulk_import/trackers.rb
+++ b/spec/factories/bulk_import/trackers.rb
@@ -24,5 +24,13 @@ FactoryBot.define do
trait :skipped do
status { -2 }
end
+
+ trait :batched do
+ batched { true }
+ end
+
+ trait :stale do
+ created_at { 1.day.ago }
+ end
end
end
diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/ci/external_pull_requests.rb
index 470814f4360..9a16e400101 100644
--- a/spec/factories/external_pull_requests.rb
+++ b/spec/factories/ci/external_pull_requests.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :external_pull_request do
+ factory :external_pull_request, class: 'Ci::ExternalPullRequest' do
sequence(:pull_request_iid)
project
source_branch { 'feature' }
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 2b6bddd2f6d..ef65cb3ec33 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -95,6 +95,10 @@ FactoryBot.define do
status { :failed }
end
+ trait :skipped do
+ status { :skipped }
+ end
+
trait :unlocked do
locked { Ci::Pipeline.lockeds[:unlocked] }
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 0f564afe822..2a09c385d66 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -55,7 +55,7 @@ FactoryBot.define do
end
trait :for_issue do
- target { association(:issue, issue_type: :issue) }
+ target { association(:issue) }
target_type { 'Issue' }
end
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index a927f0fb501..a89edc19cc7 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -90,10 +90,19 @@ FactoryBot.define do
end
end
+ factory :bamboo_integration, class: 'Integrations::Bamboo' do
+ project
+ active { true }
+ bamboo_url { 'https://bamboo.example.com' }
+ build_key { 'foo' }
+ username { 'mic' }
+ password { 'password' }
+ end
+
factory :drone_ci_integration, class: 'Integrations::DroneCi' do
project
active { true }
- drone_url { 'https://bamboo.example.com' }
+ drone_url { 'https://drone.example.com' }
token { 'test' }
end
@@ -127,6 +136,8 @@ FactoryBot.define do
jira_auth_type: evaluator.jira_auth_type,
jira_issue_transition_automatic: evaluator.jira_issue_transition_automatic,
jira_issue_transition_id: evaluator.jira_issue_transition_id,
+ jira_issue_prefix: evaluator.jira_issue_prefix,
+ jira_issue_regex: evaluator.jira_issue_regex,
username: evaluator.username, password: evaluator.password, issues_enabled: evaluator.issues_enabled,
project_key: evaluator.project_key, vulnerabilities_enabled: evaluator.vulnerabilities_enabled,
vulnerabilities_issuetype: evaluator.vulnerabilities_issuetype, deployment_type: evaluator.deployment_type
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 67824a10288..062e5294e4f 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -8,7 +8,6 @@ FactoryBot.define do
author { project.creator }
updated_by { author }
relative_position { RelativePositioning::START_POSITION }
- issue_type { :issue }
association :work_item_type, :default
trait :confidential do
@@ -66,38 +65,35 @@ FactoryBot.define do
end
end
+ trait :issue do
+ association :work_item_type, :default, :issue
+ end
+
trait :requirement do
- issue_type { :requirement }
association :work_item_type, :default, :requirement
end
trait :task do
- issue_type { :task }
association :work_item_type, :default, :task
end
trait :objective do
- issue_type { :objective }
association :work_item_type, :default, :objective
end
trait :key_result do
- issue_type { :key_result }
association :work_item_type, :default, :key_result
end
trait :incident do
- issue_type { :incident }
association :work_item_type, :default, :incident
end
trait :test_case do
- issue_type { :test_case }
association :work_item_type, :default, :test_case
end
factory :incident do
- issue_type { :incident }
association :work_item_type, :default, :incident
# An escalation status record is created for all incidents
diff --git a/spec/factories/ml/model_versions.rb b/spec/factories/ml/model_versions.rb
new file mode 100644
index 00000000000..5ae0446b78d
--- /dev/null
+++ b/spec/factories/ml/model_versions.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_model_versions, class: '::Ml::ModelVersion' do
+ sequence(:version) { |n| "version#{n}" }
+
+ model { association :ml_models }
+ project { model.project }
+
+ trait :with_package do
+ package do
+ association :ml_model_package, name: model.name, version: version, project_id: project.id
+ end
+ end
+ end
+end
diff --git a/spec/factories/ml/models.rb b/spec/factories/ml/models.rb
new file mode 100644
index 00000000000..2d1b29289a5
--- /dev/null
+++ b/spec/factories/ml/models.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_models, class: '::Ml::Model' do
+ sequence(:name) { |n| "model#{n}" }
+
+ project
+ default_experiment { association :ml_experiments, project_id: project.id, name: name }
+ end
+end
diff --git a/spec/factories/organizations/organization_settings.rb b/spec/factories/organizations/organization_settings.rb
new file mode 100644
index 00000000000..ad4715ee653
--- /dev/null
+++ b/spec/factories/organizations/organization_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :organization_setting, class: 'Organizations::OrganizationSetting' do
+ organization { association(:organization) }
+ end
+end
diff --git a/spec/factories/organizations/organization_users.rb b/spec/factories/organizations/organization_users.rb
new file mode 100644
index 00000000000..761f260ccb3
--- /dev/null
+++ b/spec/factories/organizations/organization_users.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :organization_user, class: 'Organizations::OrganizationUser' do
+ user
+ organization
+ end
+end
diff --git a/spec/factories/packages/packages.rb b/spec/factories/packages/packages.rb
index 75f540fabbe..132152bf028 100644
--- a/spec/factories/packages/packages.rb
+++ b/spec/factories/packages/packages.rb
@@ -303,7 +303,7 @@ FactoryBot.define do
factory :ml_model_package do
sequence(:name) { |n| "mlmodel-package-#{n}" }
- version { '1.0.0' }
+ sequence(:version) { |n| "v1.0.#{n}" }
package_type { :ml_model }
end
end
diff --git a/spec/factories/project_authorizations.rb b/spec/factories/project_authorizations.rb
index ffdf5576f84..1726da55c99 100644
--- a/spec/factories/project_authorizations.rb
+++ b/spec/factories/project_authorizations.rb
@@ -6,4 +6,8 @@ FactoryBot.define do
project
access_level { Gitlab::Access::REPORTER }
end
+
+ trait :owner do
+ access_level { Gitlab::Access::OWNER }
+ end
end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 3e70b897df6..34797bd933e 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -28,6 +28,7 @@ FactoryBot.define do
deployment_events { true }
feature_flag_events { true }
releases_events { true }
+ emoji_events { true }
end
trait :with_push_branch_filter do
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index 10764457d84..1e47dc0e348 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -7,7 +7,6 @@ FactoryBot.define do
author { project.creator }
updated_by { author }
relative_position { RelativePositioning::START_POSITION }
- issue_type { :issue }
association :work_item_type, :default
trait :confidential do
@@ -27,23 +26,23 @@ FactoryBot.define do
closed_at { Time.now }
end
+ trait :issue do
+ association :work_item_type, :default, :issue
+ end
+
trait :task do
- issue_type { :task }
association :work_item_type, :default, :task
end
trait :incident do
- issue_type { :incident }
association :work_item_type, :default, :incident
end
trait :requirement do
- issue_type { :requirement }
association :work_item_type, :default, :requirement
end
trait :test_case do
- issue_type { :test_case }
association :work_item_type, :default, :test_case
end
@@ -52,12 +51,10 @@ FactoryBot.define do
end
trait :objective do
- issue_type { :objective }
association :work_item_type, :default, :objective
end
trait :key_result do
- issue_type { :key_result }
association :work_item_type, :default, :key_result
end
diff --git a/spec/factories/work_items/work_item_types.rb b/spec/factories/work_items/work_item_types.rb
index d36cb6260c6..899d4297fec 100644
--- a/spec/factories/work_items/work_item_types.rb
+++ b/spec/factories/work_items/work_item_types.rb
@@ -23,6 +23,11 @@ FactoryBot.define do
namespace { nil }
end
+ trait :issue do
+ base_type { WorkItems::Type.base_types[:issue] }
+ icon_name { 'issue-type-issue' }
+ end
+
trait :incident do
base_type { WorkItems::Type.base_types[:incident] }
icon_name { 'issue-type-incident' }
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index fcf0c43243f..47a90efab1e 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -18,12 +18,12 @@ RSpec.configure(&:disable_monkey_patching!)
require 'active_support/all'
require 'pry'
+require 'gitlab/utils/all'
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_relative '../lib/gitlab'
require_relative 'simplecov_env'
SimpleCovEnv.start!
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index 82b7379b67c..ae3859280b1 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
before do
sign_in(reporter1)
stub_feature_flags(moved_mr_sidebar: false)
+ stub_feature_flags(user_profile_overflow_menu_vue: false)
end
describe 'report abuse to administrator' do
@@ -122,6 +123,10 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
end
end
+ # TODO: implement tests before the FF "user_profile_overflow_menu_vue" is turned on
+ # See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
+ # Related Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/416983
+
private
def fill_and_submit_abuse_category_form(category = "They're posting spam.")
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index ee8f94d6658..b4f64cbfa7b 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -46,10 +46,12 @@ RSpec.describe 'Admin::Hooks', feature_category: :webhooks do
it 'adds new hook' do
visit admin_hooks_path
+
+ click_button 'Add new webhook'
fill_in 'hook_url', with: url
check 'Enable SSL verification'
- expect { click_button 'Add system hook' }.to change(SystemHook, :count).by(1)
+ expect { click_button 'Add webhook' }.to change(SystemHook, :count).by(1)
expect(page).to have_content 'SSL Verification: enabled'
expect(page).to have_current_path(admin_hooks_path, ignore_query: true)
expect(page).to have_content(url)
@@ -119,11 +121,12 @@ RSpec.describe 'Admin::Hooks', feature_category: :webhooks do
it 'adds new hook' do
visit admin_hooks_path
+ click_button 'Add new webhook'
fill_in 'hook_url', with: url
uncheck 'Repository update events'
check 'Merge request events'
- expect { click_button 'Add system hook' }.to change(SystemHook, :count).by(1)
+ expect { click_button 'Add webhook' }.to change(SystemHook, :count).by(1)
expect(page).to have_current_path(admin_hooks_path, ignore_query: true)
expect(page).to have_content(url)
end
diff --git a/spec/features/admin/admin_mode/login_spec.rb b/spec/features/admin/admin_mode/login_spec.rb
index c0c8b12342a..72c7083f459 100644
--- a/spec/features/admin/admin_mode/login_spec.rb
+++ b/spec/features/admin/admin_mode/login_spec.rb
@@ -139,8 +139,10 @@ RSpec.describe 'Admin Mode Login', feature_category: :system_access do
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
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index b81703f728b..7fb2202ca1d 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -32,30 +32,13 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
describe "runners registration" do
- context 'when create_runner_workflow_for_namespace is enabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_admin: true)
-
- visit admin_runners_path
- end
-
- it_behaves_like "shows and resets runner registration token" do
- let(:dropdown_text) { s_('Runners|Register an instance runner') }
- let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
- end
+ before do
+ visit admin_runners_path
end
- context 'when create_runner_workflow_for_namespace is disabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_admin: false)
-
- visit admin_runners_path
- end
-
- it_behaves_like "shows and resets runner registration token" do
- let(:dropdown_text) { s_('Runners|Register an instance runner') }
- let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
- end
+ it_behaves_like "shows and resets runner registration token" do
+ let(:dropdown_text) { s_('Runners|Register an instance runner') }
+ let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 3e08d2277c1..b78d6777a1a 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -8,11 +8,9 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
include UsageDataHelpers
let_it_be(:admin) { create(:admin) }
- let(:dot_com?) { false }
context 'application setting :admin_mode is enabled', :request_store do
before do
- allow(Gitlab).to receive(:com?).and_return(dot_com?)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
@@ -147,9 +145,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
context 'Dormant users', feature_category: :user_management do
- context 'when Gitlab.com' do
- let(:dot_com?) { true }
-
+ context 'when Gitlab.com', :saas do
it 'does not expose the setting section' do
# NOTE: not_to have_content may have false positives for content
# that might not load instantly, so before checking that
@@ -163,8 +159,6 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
context 'when not Gitlab.com' do
- let(:dot_com?) { false }
-
it 'exposes the setting section' do
expect(page).to have_content('Dormant users')
expect(page).to have_field('Deactivate dormant users after a period of inactivity')
@@ -366,9 +360,32 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end
context 'GitLab for Slack app settings', feature_category: :integrations do
+ let(:create_heading) { 'Create your GitLab for Slack app' }
+ let(:configure_heading) { 'Configure the app settings' }
+ let(:update_heading) { 'Update your Slack app' }
+
+ it 'has all sections' do
+ page.within('.as-slack') do
+ expect(page).to have_content(create_heading)
+ expect(page).to have_content(configure_heading)
+ expect(page).to have_content(update_heading)
+ end
+ end
+
+ context 'when GitLab.com', :saas do
+ it 'only has the configure section' do
+ page.within('.as-slack') do
+ expect(page).to have_content(configure_heading)
+
+ expect(page).not_to have_content(create_heading)
+ expect(page).not_to have_content(update_heading)
+ end
+ end
+ end
+
it 'changes the settings' do
page.within('.as-slack') do
- check 'Enable Slack application'
+ check 'Enable GitLab for Slack app'
fill_in 'Client ID', with: 'slack_app_id'
fill_in 'Client secret', with: 'slack_app_secret'
fill_in 'Signing secret', with: 'slack_app_signing_secret'
@@ -775,6 +792,18 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
expect(current_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else])
end
+ it 'changes gitlab shell operation limits settings' do
+ visit network_admin_application_settings_path
+
+ page.within('[data-testid="gitlab-shell-operation-limits"]') do
+ fill_in 'Maximum number of Git operations per minute', with: 100
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.gitlab_shell_operation_limit).to eq(100)
+ end
+
it 'changes Projects API rate limits settings' do
visit network_admin_application_settings_path
diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
index d0ca5d76cc7..881ccec017b 100644
--- a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User activates the instance-level Mattermost Slash Command integration', :js,
-feature_category: :integrations do
+ feature_category: :integrations do
include_context 'instance integration activation'
before do
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index f8f1fdaabb4..a95fd133133 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -260,7 +260,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
it 'logs in as the user when impersonate is clicked' do
subject
- find('[data-testid="user-menu"]').click
+ find('[data-testid="user-dropdown"]').click
expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eql(another_user.username)
end
@@ -317,7 +317,7 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do
it 'logs out of impersonated user back to original user' do
subject
- find('[data-testid="user-menu"]').click
+ find('[data-testid="user-dropdown"]').click
expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eq(current_user.username)
end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 89db70c6680..bd5903efe10 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -49,8 +49,11 @@ RSpec.describe 'Issues Feed', feature_category: :devops_reports do
before do
personal_access_token = create(:personal_access_token, user: user)
- visit project_issues_path(project, :atom,
- private_token: personal_access_token.token)
+ visit project_issues_path(
+ project,
+ :atom,
+ private_token: personal_access_token.token
+ )
end
it_behaves_like 'an authenticated issuable atom feed'
@@ -59,8 +62,11 @@ RSpec.describe 'Issues Feed', feature_category: :devops_reports do
context 'when authenticated via feed token' do
before do
- visit project_issues_path(project, :atom,
- feed_token: user.feed_token)
+ visit project_issues_path(
+ project,
+ :atom,
+ feed_token: user.feed_token
+ )
end
it_behaves_like 'an authenticated issuable atom feed'
diff --git a/spec/features/atom/merge_requests_spec.rb b/spec/features/atom/merge_requests_spec.rb
index b9e1c7042b2..0238380da90 100644
--- a/spec/features/atom/merge_requests_spec.rb
+++ b/spec/features/atom/merge_requests_spec.rb
@@ -46,8 +46,11 @@ RSpec.describe 'Merge Requests Feed', feature_category: :devops_reports do
before do
personal_access_token = create(:personal_access_token, user: user)
- visit project_merge_requests_path(project, :atom,
- private_token: personal_access_token.token)
+ visit project_merge_requests_path(
+ project,
+ :atom,
+ private_token: personal_access_token.token
+ )
end
it_behaves_like 'an authenticated issuable atom feed'
@@ -56,8 +59,11 @@ RSpec.describe 'Merge Requests Feed', feature_category: :devops_reports do
context 'when authenticated via feed token' do
before do
- visit project_merge_requests_path(project, :atom,
- feed_token: user.feed_token)
+ visit project_merge_requests_path(
+ project,
+ :atom,
+ feed_token: user.feed_token
+ )
end
it_behaves_like 'an authenticated issuable atom feed'
diff --git a/spec/features/atom/topics_spec.rb b/spec/features/atom/topics_spec.rb
new file mode 100644
index 00000000000..078c5b55eeb
--- /dev/null
+++ b/spec/features/atom/topics_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Topic Feed", feature_category: :groups_and_projects do
+ let_it_be(:topic) { create(:topic, name: 'test-topic', title: 'Test topic') }
+ let_it_be(:empty_topic) { create(:topic, name: 'test-empty-topic', title: 'Test empty topic') }
+ let_it_be(:project1) { create(:project, :public, topic_list: topic.name) }
+ let_it_be(:project2) { create(:project, :public, topic_list: topic.name) }
+
+ context 'when topic does not exist' do
+ let(:path) { topic_explore_projects_path(topic_name: 'non-existing', format: 'atom') }
+
+ it 'renders 404' do
+ visit path
+
+ expect(status_code).to eq(404)
+ end
+ end
+
+ context 'when topic exists' do
+ before do
+ visit topic_explore_projects_path(topic_name: topic.name, format: 'atom')
+ end
+
+ it "renders topic atom feed" do
+ expect(body).to have_selector('feed title')
+ end
+
+ it "has project entries" do
+ expect(body).to have_content(project1.name)
+ expect(body).to have_content(project2.name)
+ end
+ end
+
+ context 'when topic is empty' do
+ before do
+ visit topic_explore_projects_path(topic_name: empty_topic.name, format: 'atom')
+ end
+
+ it "renders topic atom feed" do
+ expect(body).to have_selector('feed title')
+ end
+
+ it "has no project entry" do
+ expect(body).to have_no_selector('entry')
+ end
+ end
+end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index b743f900ae7..f801f93686c 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -25,27 +25,33 @@ RSpec.describe "User Feed", feature_category: :devops_reports do
context 'feed content' do
let(:project) { create(:project, :repository) }
let(:issue) do
- create(:issue,
- project: project,
- author: user,
- description: "Houston, we have a bug!\n\n***\n\nI guess.")
+ create(
+ :issue,
+ project: project,
+ author: user,
+ description: "Houston, we have a bug!\n\n***\n\nI guess."
+ )
end
let(:note) do
- create(:note,
- noteable: issue,
- author: user,
- note: 'Bug confirmed :+1:',
- project: project)
+ create(
+ :note,
+ noteable: issue,
+ author: user,
+ note: 'Bug confirmed :+1:',
+ project: project
+ )
end
let(:merge_request) do
- create(:merge_request,
- title: 'Fix bug',
- author: user,
- source_project: project,
- target_project: project,
- description: "Here is the fix: ![an image](image.png)")
+ create(
+ :merge_request,
+ title: 'Fix bug',
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "Here is the fix: ![an image](image.png)"
+ )
end
let(:push_event) { create(:push_event, project: project, author: user) }
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 1ea6e079104..85e54c0f451 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -74,7 +74,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
let_it_be(:a_plus, reload: true) { create(:label, project: project, name: 'A+') }
let_it_be(:list1, reload: true) { create(:list, board: board, label: planning, position: 0) }
let_it_be(:list2, reload: true) { create(:list, board: board, label: development, position: 1) }
- let_it_be(:backlog_list, reload: true) { create(:backlog_list, board: board) }
let_it_be(:confidential_issue, reload: true) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let_it_be(:issue1, reload: true) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
@@ -591,8 +590,6 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
def remove_list
page.within(find('.board:nth-child(2)')) do
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button('Edit list settings')
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index b6196fa6a1d..35e387c9d8a 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -220,12 +220,14 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
end
def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000)
- drag_to(selector: selector,
- scrollable: '#board-app',
- list_from_index: list_from_index,
- from_index: from_index,
- to_index: to_index,
- list_to_index: list_to_index,
- duration: duration)
+ drag_to(
+ selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index,
+ duration: duration
+ )
end
end
diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb
index 7afe34be3d8..03b1643d7c4 100644
--- a/spec/features/boards/multi_select_spec.rb
+++ b/spec/features/boards/multi_select_spec.rb
@@ -11,13 +11,15 @@ RSpec.describe 'Multi Select Issue', :js, feature_category: :team_planning do
let(:user) { create(:user) }
def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000)
- drag_to(selector: selector,
- scrollable: '#board-app',
- list_from_index: list_from_index,
- from_index: from_index,
- to_index: to_index,
- list_to_index: list_to_index,
- duration: duration)
+ drag_to(
+ selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index,
+ duration: duration
+ )
end
def wait_for_board_cards(board_number, expected_cards)
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 1fcea45c7ae..682ccca38bd 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:board) { create(:board, project: project) }
- let_it_be(:backlog_list) { create(:backlog_list, board: board) }
let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
let_it_be(:user) { create(:user) }
@@ -32,22 +31,17 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
end
it 'displays new issue button' do
- 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_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
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button 'Create new issue'
expect(page).to have_selector('.board-new-issue-form')
@@ -56,8 +50,6 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
it 'hides form when clicking cancel' do
page.within(first('.board')) do
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button 'Create new issue'
expect(page).to have_selector('.board-new-issue-form')
@@ -70,8 +62,6 @@ 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
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button 'Create new issue'
end
@@ -100,8 +90,6 @@ 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
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button 'Create new issue'
end
@@ -132,8 +120,6 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
wait_for_all_requests
page.within('.board:nth-child(2)') do
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button('Create new issue')
page.within(first('.board-new-issue-form')) do
@@ -157,13 +143,11 @@ 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(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_selector("[data-testid='header-list-actions']")
expect(page).not_to have_button('Create new issue')
end
end
@@ -188,23 +172,18 @@ 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_selector("[data-testid='header-list-actions']")
expect(page).not_to have_button('Create new issue')
end
end
end
context 'when backlog list already exists' do
- let_it_be(:backlog_list) { create(:backlog_list, board: group_board) }
-
it 'does not display new issue button in open list' do
- 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_selector("[data-testid='header-list-actions']")
expect(page).not_to have_button('Create new issue')
end
end
@@ -222,20 +201,18 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
end
context 'when backlog does not exist' do
+ before do
+ group_board.lists.backlog.delete_all
+ end
+
it 'display new issue button in label list' do
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
expect(board_list_header).to have_button('Create new issue')
end
end
context 'project select dropdown' do
- let_it_be(:backlog_list) { create(:backlog_list, board: group_board) }
-
before do
page.within(board_list_header) do
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
click_button 'Create new issue'
end
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index a912ea28ddc..899ab5863e1 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards sidebar assignee', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332078',
- feature_category: :team_planning do
+RSpec.describe 'Project issue boards sidebar assignee', :js,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332078',
+ feature_category: :team_planning do
include BoardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index 39485fe21a9..ffed4a0854f 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -12,7 +12,6 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea
context 'group boards' do
context 'in the top-level group board' do
let_it_be(:group_board) { create(:board, group: group) }
- let_it_be(:board_list) { create(:backlog_list, board: group_board) }
before do
stub_feature_flags(apollo_boards: false)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 358da1e1279..4807b691e4f 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_planning do
+RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_planning, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/416414' do
include BoardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index a936e14168c..cc2afca7657 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -13,8 +13,6 @@ RSpec.describe 'User adds lists', :js, feature_category: :team_planning do
let_it_be(:group_label) { create(:group_label, group: group) }
let_it_be(:project_label) { create(:label, project: project) }
- let_it_be(:group_backlog_list) { create(:backlog_list, board: group_board) }
- let_it_be(:project_backlog_list) { create(:backlog_list, board: project_board) }
let_it_be(:backlog) { create(:group_label, group: group, name: 'Backlog') }
let_it_be(:closed) { create(:group_label, group: group, name: 'Closed') }
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index 44c691435df..5867ec17070 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -62,7 +62,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
context "project boards" do
stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, project: project) }
- let_it_be(:backlog_list) { create_default(:backlog_list, board: board) }
let(:board_path) { project_boards_path(project, params) }
@@ -72,7 +71,6 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
context "group boards" do
stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, group: group) }
- let_it_be(:backlog_list) { create_default(:backlog_list, board: board) }
let(:board_path) { group_boards_path(group, params) }
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
index 93a49151978..d90c43f452c 100644
--- a/spec/features/clusters/create_agent_spec.rb
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -27,7 +27,8 @@ RSpec.describe 'Cluster agent registration', :js, feature_category: :deployment_
end
it 'allows the user to select an agent to install, and displays the resulting agent token' do
- click_button('Connect a cluster')
+ find('[data-testid="clusters-default-action-button"]').click
+
expect(page).to have_content('Register')
click_button('Select an agent or enter a name to create new')
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index fd09a7f7343..b72e08b854e 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -8,20 +8,21 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
describe 'CI' do
before do
- stub_feature_flags(pipeline_details_header_vue: false)
sign_in(user)
stub_ci_pipeline_to_return_yaml_file
end
let(:creator) { create(:user, developer_projects: [project]) }
let!(:pipeline) do
- create(:ci_pipeline,
- project: project,
- user: creator,
- ref: project.default_branch,
- sha: project.commit.sha,
- status: :success,
- created_at: 5.months.ago)
+ create(
+ :ci_pipeline,
+ project: project,
+ user: creator,
+ ref: project.default_branch,
+ sha: project.commit.sha,
+ status: :success,
+ created_at: 5.months.ago
+ )
end
context 'commit status is Generic Commit Status' do
@@ -39,7 +40,11 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
wait_for_requests
end
- it { expect(page).to have_content pipeline.sha[0..7] }
+ it 'contains commit short id' do
+ page.within('[data-testid="pipeline-details-header"]') do
+ expect(page).to have_content pipeline.sha[0..7]
+ end
+ end
it 'contains generic commit status build' do
page.within('[data-testid="jobs-tab-table"]') do
@@ -61,11 +66,13 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
describe 'Project commits' do
let!(:pipeline_from_other_branch) do
- create(:ci_pipeline,
- project: project,
- ref: 'fix',
- sha: project.commit.sha,
- status: :failed)
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: 'fix',
+ sha: project.commit.sha,
+ status: :failed
+ )
end
before do
@@ -88,7 +95,6 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
it 'shows pipeline data' do
expect(page).to have_content pipeline.sha[0..7]
- expect(page).to have_content pipeline.git_commit_message.gsub!(/\s+/, ' ')
expect(page).to have_content pipeline.user.name
end
end
@@ -116,7 +122,7 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
describe 'Cancel build' do
it 'cancels build', :js, :sidekiq_might_not_need_inline do
visit pipeline_path(pipeline)
- find('[data-testid="cancelPipeline"]').click
+ find('[data-testid="cancel-pipeline"]').click
expect(page).to have_content 'canceled'
end
end
@@ -132,7 +138,6 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
it 'renders header' do
expect(page).to have_content pipeline.sha[0..7]
- expect(page).to have_content pipeline.git_commit_message.gsub!(/\s+/, ' ')
expect(page).to have_content pipeline.user.name
expect(page).not_to have_link('Cancel pipeline')
expect(page).not_to have_link('Retry')
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index 2345e4be722..60621f57bde 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -59,12 +59,14 @@ RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do
let!(:push_event) do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload,
- event: event,
- action: :created,
- commit_to: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
- ref: 'new_design',
- commit_count: 1)
+ create(
+ :push_event_payload,
+ event: event,
+ action: :created,
+ commit_to: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
+ ref: 'new_design',
+ commit_count: 1
+ )
event
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index c6e78c8b57c..e84a3c8cc66 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -18,8 +18,13 @@ RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :user_profil
context 'on the activity tab' do
before do
- Event.create!(project: project, author_id: user.id, action: :joined,
- updated_at: created_date, created_at: created_date)
+ Event.create!(
+ project: project,
+ author_id: user.id,
+ action: :joined,
+ updated_at: created_date,
+ created_at: created_date
+ )
sign_in user
visit user_activity_path(user)
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index 964ac2f714d..ab3aa29a3aa 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -61,10 +61,15 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
- expect(params).to include('feed_token' => [user.feed_token])
+ feed_token_param = params['feed_token']
+ expect(feed_token_param).to match([Gitlab::Auth::AuthFinders::PATH_DEPENDENT_FEED_TOKEN_REGEX])
+ expect(feed_token_param.first).to end_with(user.id.to_s)
expect(params).to include('milestone_title' => [''])
expect(params).to include('assignee_username' => [user.username.to_s])
- expect(auto_discovery_params).to include('feed_token' => [user.feed_token])
+
+ feed_token_param = auto_discovery_params['feed_token']
+ expect(feed_token_param).to match([Gitlab::Auth::AuthFinders::PATH_DEPENDENT_FEED_TOKEN_REGEX])
+ expect(feed_token_param.first).to end_with(user.id.to_s)
expect(auto_discovery_params).to include('milestone_title' => [''])
expect(auto_discovery_params).to include('assignee_username' => [user.username.to_s])
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index d53f5affe64..624f3530f81 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -79,39 +79,52 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
end
let!(:assigned_merge_request_from_fork) do
- create(:merge_request,
- source_branch: 'markdown', assignees: [current_user],
- target_project: public_project, source_project: forked_project,
- author: author_user)
+ create(
+ :merge_request,
+ source_branch: 'markdown',
+ assignees: [current_user],
+ target_project: public_project,
+ source_project: forked_project,
+ author: author_user
+ )
end
let!(:authored_merge_request) do
- create(:merge_request,
- source_branch: 'markdown',
- source_project: project,
- author: current_user)
+ create(
+ :merge_request,
+ source_branch: 'markdown',
+ source_project: project,
+ author: current_user
+ )
end
let!(:authored_merge_request_from_fork) do
- create(:merge_request,
- source_branch: 'feature_conflict',
- author: current_user,
- target_project: public_project, source_project: forked_project)
+ create(
+ :merge_request,
+ source_branch: 'feature_conflict',
+ author: current_user,
+ target_project: public_project,
+ source_project: forked_project
+ )
end
let!(:labeled_merge_request) do
- create(:labeled_merge_request,
- source_branch: 'labeled',
- labels: [label],
- author: current_user,
- source_project: project)
+ create(
+ :labeled_merge_request,
+ source_branch: 'labeled',
+ labels: [label],
+ author: current_user,
+ source_project: project
+ )
end
let!(:other_merge_request) do
- create(:merge_request,
- source_branch: 'fix',
- source_project: project,
- author: author_user)
+ create(
+ :merge_request,
+ source_branch: 'fix',
+ source_project: project,
+ author: author_user
+ )
end
before do
diff --git a/spec/features/dashboard/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb
index e449f71878b..e1460e345fc 100644
--- a/spec/features/dashboard/todos/todos_sorting_spec.rb
+++ b/spec/features/dashboard/todos/todos_sorting_spec.rb
@@ -27,8 +27,9 @@ RSpec.describe 'Dashboard > User sorts todos', feature_category: :team_planning
create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago, updated_at: 4.hours.ago)
create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago, updated_at: 2.minutes.ago)
create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago, updated_at: 2.hours.ago)
- create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago,
- updated_at: 1.hour.ago)
+ create(
+ :todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago, updated_at: 1.hour.ago
+ )
merge_request_1.labels << label_1
issue_3.labels << label_1
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index d0003b69415..9d59126df8d 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -443,12 +443,15 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
let_it_be(:target) { create(:design, issue: issue, project: project) }
let_it_be(:note) { create(:note, project: project, note: 'I am note, hear me roar') }
let_it_be(:todo) do
- create(:todo, :mentioned,
- user: user,
- project: project,
- target: target,
- author: author,
- note: note)
+ create(
+ :todo,
+ :mentioned,
+ user: user,
+ project: project,
+ target: target,
+ author: author,
+ note: note
+ )
end
before do
@@ -467,10 +470,12 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
context 'User requested access' do
shared_examples 'has todo present with access request content' do
specify do
- create(:todo, :member_access_requested,
- user: user,
- target: target,
- author: author
+ create(
+ :todo,
+ :member_access_requested,
+ user: user,
+ target: target,
+ author: author
)
target.add_owner(user)
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
index 90be3f0760d..b270a4c7600 100644
--- a/spec/features/discussion_comments/issue_spec.rb
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Thread Comments Issue', :js, feature_category: :source_code_management do
+ include ContentEditorHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -12,6 +14,7 @@ RSpec.describe 'Thread Comments Issue', :js, feature_category: :source_code_mana
sign_in(user)
visit project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
end
it_behaves_like 'thread comments for issue, epic and merge request', 'issue'
diff --git a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
index 168c4f330ca..5efcb5f8b8e 100644
--- a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
+++ b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'When a user filters Sentry errors by status', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
-feature_category: :error_tracking do
+ feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
diff --git a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
index 6026b42f7de..d4c537f1939 100644
--- a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
+++ b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
-feature_category: :error_tracking do
+ feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
diff --git a/spec/features/error_tracking/user_sees_error_details_spec.rb b/spec/features/error_tracking/user_sees_error_details_spec.rb
index d7676d90d21..8fcf5df41c7 100644
--- a/spec/features/error_tracking/user_sees_error_details_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_details_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
-feature_category: :error_tracking do
+ feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
context 'with current user as project owner' do
diff --git a/spec/features/error_tracking/user_sees_error_index_spec.rb b/spec/features/error_tracking/user_sees_error_index_spec.rb
index f83c8ffe439..e86e89ad058 100644
--- a/spec/features/error_tracking/user_sees_error_index_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_index_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'View error index page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
-feature_category: :error_tracking do
+ feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
diff --git a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
index c4c5eb6b74b..aeb8fd11170 100644
--- a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
+++ b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js, feature_categ
# These keys are rejected directly by rack itself.
# The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example)
- it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (Rack::QueryParser::ParamsTooDeepError)'
+ it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, status: 400, message: 'Bad Request'
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', status: 400, message: 'Bad Request'
it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key'
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index 25f7d4d968c..c2d6b80b4c0 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -25,8 +25,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
it 'adds an issue to the backlog' do
page.within(find('.board', match: :first)) do
- dropdown = first("[data-testid='header-list-actions']")
- dropdown.click
issue_title = 'Create new issue'
click_button issue_title
@@ -52,7 +50,6 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
context "when user is a Reporter in one of the group's projects", :js do
let_it_be(:board) { create(:board, group: group) }
- let_it_be(:backlog_list) { create(:backlog_list, board: board) }
let_it_be(:group_label1) { create(:group_label, title: "bug", group: group) }
let_it_be(:group_label2) { create(:group_label, title: "dev", group: group) }
let_it_be(:list1) { create(:list, board: board, label: group_label1, position: 0) }
diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb
index 514110d78ae..e9d2d185e8a 100644
--- a/spec/features/groups/group_runners_spec.rb
+++ b/spec/features/groups/group_runners_spec.rb
@@ -16,21 +16,6 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
end
describe "Group runners page", :js do
- describe "legacy runners registration" do
- let_it_be(:group_registration_token) { group.runners_token }
-
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
-
- visit group_runners_path(group)
- end
-
- it_behaves_like "shows and resets runner registration token" do
- let(:dropdown_text) { 'Register a group runner' }
- let(:registration_token) { group_registration_token }
- end
- end
-
context "with no runners" do
before do
visit group_runners_path(group)
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 0a697eaa798..d870471d646 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
+ include ContentEditorHelpers
+
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project_empty_repo, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
@@ -18,6 +20,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
context 'create a milestone', :js do
before do
visit new_group_milestone_path(group)
+ close_rich_text_promo_popover_if_present
end
it 'renders description preview' do
@@ -27,7 +30,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
click_button("Preview")
- preview = find('.js-md-preview')
+ preview = find('.js-vue-md-preview')
expect(preview).to have_content('Nothing to preview.')
@@ -66,6 +69,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
context 'when no milestones' do
it 'renders no milestones text' do
visit group_milestones_path(group)
+ close_rich_text_promo_popover_if_present
expect(page).to have_content('Use milestones to track issues and merge requests')
end
end
@@ -95,6 +99,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
before do
visit group_milestones_path(group)
+ close_rich_text_promo_popover_if_present
end
it 'counts milestones correctly' do
@@ -170,6 +175,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
before do
visit group_milestone_path(group, milestone)
+ close_rich_text_promo_popover_if_present
end
it 'renders the issues tab' do
diff --git a/spec/features/groups/milestones/gfm_autocomplete_spec.rb b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
index 8df097dde88..9245323d1f7 100644
--- a/spec/features/groups/milestones/gfm_autocomplete_spec.rb
+++ b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
+ include Features::AutocompleteHelpers
+
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:project) { create(:project, :repository, group: group) }
@@ -69,10 +71,6 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
private
- def find_autocomplete_menu
- find('.atwho-view ul', visible: true)
- end
-
def expect_autocomplete_entry(entry)
page.within('.atwho-container') do
expect(page).to have_content(entry)
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index b7f9cd3e93a..ec8215928e4 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'Group Packages', feature_category: :package_registry do
it_behaves_like 'package details link'
it 'allows you to navigate to the project page' do
- find('[data-testid="root-link"]', text: project.path).click
+ find('[data-testid="root-link"]', text: project.name).click
expect(page).to have_current_path(project_path(project))
expect(page).to have_content(project.name)
diff --git a/spec/features/groups/participants_autocomplete_spec.rb b/spec/features/groups/participants_autocomplete_spec.rb
new file mode 100644
index 00000000000..a94f95c3ced
--- /dev/null
+++ b/spec/features/groups/participants_autocomplete_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Group member autocomplete', :js, feature_category: :groups_and_projects do
+ include Features::AutocompleteHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ group.add_developer user
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when editing description of a group milestone' do
+ let_it_be(:noteable) { create(:milestone, group: group) }
+
+ it 'suggests group members' do
+ visit edit_group_milestone_path(group, noteable)
+
+ fill_in 'Description', with: '@'
+
+ expect(find_autocomplete_menu).to have_text(user.username)
+ end
+
+ context 'for a member of a private group invited to the group' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:private_group_member) { create(:user, username: 'private-a') }
+
+ before_all do
+ private_group.add_developer private_group_member
+
+ create(:group_group_link, shared_group: group, shared_with_group: private_group)
+ end
+
+ it 'suggests member of private group as well' do
+ visit edit_group_milestone_path(group, noteable)
+
+ fill_in 'Description', with: '@'
+
+ expect(find_autocomplete_menu).to have_text(private_group_member.username)
+ expect(find_autocomplete_menu).to have_text(user.username)
+ 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 cb92f9abdf5..c7e81803694 100644
--- a/spec/features/groups/settings/access_tokens_spec.rb
+++ b/spec/features/groups/settings/access_tokens_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Group > Settings > Access Tokens', :js, feature_category: :syste
it_behaves_like 'resource access tokens creation', 'group'
context 'when token creation is not allowed' do
- it_behaves_like 'resource access tokens creation disallowed', 'Group access token creation is disabled in this group. You can still use and manage existing tokens.'
+ it_behaves_like 'resource access tokens creation disallowed', 'Group access token creation is disabled in this group.'
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index de4b9964b98..67133b1856f 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -461,9 +461,11 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
describe 'new subgroup / project button' do
let_it_be(:group, reload: true) do
- create(:group,
- project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS,
- subgroup_creation_level: Gitlab::Access::OWNER_SUBGROUP_ACCESS)
+ create(
+ :group,
+ project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS,
+ subgroup_creation_level: Gitlab::Access::OWNER_SUBGROUP_ACCESS
+ )
end
before do
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 905c5e25f6e..627326dde18 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -47,9 +47,11 @@ RSpec.describe 'Help Pages', feature_category: :shared do
describe 'when help page is customized' do
before do
- stub_application_setting(help_page_hide_commercial_content: true,
- help_page_text: 'My Custom Text',
- help_page_support_url: 'http://example.com/help')
+ stub_application_setting(
+ help_page_hide_commercial_content: true,
+ help_page_text: 'My Custom Text',
+ help_page_support_url: 'http://example.com/help'
+ )
sign_in(create(:user))
visit help_path
diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb
index 7115bd7dff7..00318f83105 100644
--- a/spec/features/ics/dashboard_issues_spec.rb
+++ b/spec/features/ics/dashboard_issues_spec.rb
@@ -29,9 +29,11 @@ RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_plannin
context 'with no referer' do
it 'renders calendar feed' do
sign_in user
- visit issues_dashboard_path(:ics,
- due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
- sort: 'closest_future_date')
+ visit issues_dashboard_path(
+ :ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date'
+ )
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -42,9 +44,11 @@ RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_plannin
it 'renders calendar feed as text/plain' do
sign_in user
page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url))
- visit issues_dashboard_path(:ics,
- due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
- sort: 'closest_future_date')
+ visit issues_dashboard_path(
+ :ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date'
+ )
expect(response_headers['Content-Type']).to have_content('text/plain')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -54,10 +58,12 @@ RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_plannin
context 'when filtered by milestone' do
it 'renders calendar feed' do
sign_in user
- visit issues_dashboard_path(:ics,
- due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
- sort: 'closest_future_date',
- milestone_title: milestone.title)
+ visit issues_dashboard_path(
+ :ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ milestone_title: milestone.title
+ )
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -69,10 +75,12 @@ RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_plannin
it 'renders calendar feed' do
personal_access_token = create(:personal_access_token, user: user)
- visit issues_dashboard_path(:ics,
- due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
- sort: 'closest_future_date',
- private_token: personal_access_token.token)
+ visit issues_dashboard_path(
+ :ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ private_token: personal_access_token.token
+ )
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -81,10 +89,12 @@ RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_plannin
context 'when authenticated via feed token' do
it 'renders calendar feed' do
- visit issues_dashboard_path(:ics,
- due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
- sort: 'closest_future_date',
- feed_token: user.feed_token)
+ visit issues_dashboard_path(
+ :ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token
+ )
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -93,15 +103,24 @@ RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_plannin
context 'issue with due date' do
let!(:issue) do
- create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
- description: 'test desc', due_date: Date.tomorrow)
+ create(
+ :issue,
+ author: user,
+ assignees: [assignee],
+ project: project,
+ title: 'test title',
+ description: 'test desc',
+ due_date: Date.tomorrow
+ )
end
it 'renders issue fields' do
- visit issues_dashboard_path(:ics,
- due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
- sort: 'closest_future_date',
- feed_token: user.feed_token)
+ visit issues_dashboard_path(
+ :ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token
+ )
expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
# line length for ics is 75 chars
diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb
index 164f5df7cc5..ce9f5638a00 100644
--- a/spec/features/ics/group_issues_spec.rb
+++ b/spec/features/ics/group_issues_spec.rb
@@ -71,8 +71,15 @@ RSpec.describe 'Group Issues Calendar Feed', feature_category: :groups_and_proje
context 'issue with due date' do
let!(:issue) do
- create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
- description: 'test desc', due_date: Date.tomorrow)
+ create(
+ :issue,
+ author: user,
+ assignees: [assignee],
+ project: project,
+ title: 'test title',
+ description: 'test desc',
+ due_date: Date.tomorrow
+ )
end
it 'renders issue fields' do
diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb
index daad6f1df2f..c26147e0310 100644
--- a/spec/features/ics/project_issues_spec.rb
+++ b/spec/features/ics/project_issues_spec.rb
@@ -70,8 +70,15 @@ RSpec.describe 'Project Issues Calendar Feed', feature_category: :groups_and_pro
context 'issue with due date' do
let!(:issue) do
- create(:issue, author: user, assignees: [assignee], project: project, title: 'test title',
- description: 'test desc', due_date: Date.tomorrow)
+ create(
+ :issue,
+ author: user,
+ assignees: [assignee],
+ project: project,
+ title: 'test title',
+ description: 'test desc',
+ due_date: Date.tomorrow
+ )
end
it 'renders issue fields' do
diff --git a/spec/features/incidents/incident_timeline_events_spec.rb b/spec/features/incidents/incident_timeline_events_spec.rb
index 4d51ed652c9..bd3658ab60f 100644
--- a/spec/features/incidents/incident_timeline_events_spec.rb
+++ b/spec/features/incidents/incident_timeline_events_spec.rb
@@ -86,14 +86,14 @@ RSpec.describe 'Incident timeline events', :js, feature_category: :incident_mana
def trigger_dropdown_action(text)
click_button _('More actions')
- page.within '.gl-dropdown-contents' do
- page.find('.gl-dropdown-item', text: text).click
+ page.within '[data-testid="disclosure-content"]' do
+ page.find('[data-testid="disclosure-dropdown-item"]', text: text).click
end
end
end
it_behaves_like 'for each incident details route',
- 'add, edit, and delete timeline events',
- tab_text: s_('Incident|Timeline'),
- tab: 'timeline'
+ 'add, edit, and delete timeline events',
+ tab_text: s_('Incident|Timeline'),
+ tab: 'timeline'
end
diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb
index 8739c99bdd0..bbf579b09a8 100644
--- a/spec/features/incidents/user_views_incident_spec.rb
+++ b/spec/features/incidents/user_views_incident_spec.rb
@@ -31,10 +31,12 @@ RSpec.describe "User views incident", feature_category: :incident_management do
describe 'user actions' do
it 'shows the merge request and incident actions', :js, :aggregate_failures do
- expected_href = new_project_issue_path(project,
- issuable_template: 'incident',
- issue: { issue_type: 'incident' },
- add_related_issue: incident.iid)
+ expected_href = new_project_issue_path(
+ project,
+ issuable_template: 'incident',
+ issue: { issue_type: 'incident' },
+ add_related_issue: incident.iid
+ )
click_button 'Incident actions'
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index a1e75a94326..03ec72980e5 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -232,7 +232,8 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
end
context 'when the user signs up for an account with the invitation email address' do
- it 'redirects to the most recent membership activity page with all invitations automatically accepted' do
+ it 'redirects to the most recent membership activity page with all invitations automatically accepted',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/417092' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index c979aff2147..7bf9620f282 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -99,9 +99,7 @@ RSpec.describe 'issuable list', :js, feature_category: :team_planning do
if issuable_type == :issue
issue = Issue.reorder(:iid).first
- merge_request = create(:merge_request,
- source_project: project,
- source_branch: generate(:branch))
+ merge_request = create(:merge_request, source_project: project, source_branch: generate(:branch))
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 887bc7d0c87..e072231c6e9 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe "Jira", :js, feature_category: :team_planning do
+ include ContentEditorHelpers
+
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
@@ -24,6 +26,7 @@ RSpec.describe "Jira", :js, feature_category: :team_planning do
sign_in(user)
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
build_note
end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 7f6a044a575..d35f037247d 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -12,9 +12,9 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
url = new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
if title.empty?
- %Q{a[href="#{url}"]}
+ %{a[href="#{url}"]}
else
- %Q{a[title="#{title}"][href="#{url}"]}
+ %{a[title="#{title}"][href="#{url}"]}
end
end
@@ -30,7 +30,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
end
it 'shows a button to resolve all threads by creating a new issue' do
- find('.discussions-counter .dropdown-toggle').click
+ find('.discussions-counter .gl-new-dropdown-toggle').click
within('.discussions-counter') do
expect(page).to have_link(_("Resolve all with new issue"), href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid))
@@ -49,7 +49,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
context 'creating an issue for threads' do
before do
- find('.discussions-counter .dropdown-toggle').click
+ find('.discussions-counter .gl-new-dropdown-toggle').click
find(resolve_all_discussions_link_selector).click
end
@@ -65,7 +65,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
before do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
visit project_merge_request_path(project, merge_request)
- find('.discussions-counter .dropdown-toggle').click
+ find('.discussions-counter .gl-new-dropdown-toggle').click
end
it 'does not show a link to create a new issue' do
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index 3a32bd34af8..73a920421a3 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -74,8 +74,11 @@ RSpec.describe 'Resolve an open thread in a merge request by creating an issue',
before do
project.add_reporter(user)
sign_in user
- visit new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid,
- discussion_to_resolve: discussion.id)
+ visit new_project_issue_path(
+ project,
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id
+ )
end
it 'shows a notice to ask someone else to resolve the threads' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index a65befc3115..b9562f12ef2 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -455,19 +455,6 @@ RSpec.describe 'Filter issues', :js, feature_category: :team_planning do
expect(page).to have_content(issue.title)
end
- it 'filters issues by searched text containing special characters' do
- stub_feature_flags(issues_full_text_search: false)
-
- issue = create(:issue, project: project, author: user, title: "issue with !@\#{$%^&*()-+")
-
- search = '!@#{$%^&*()-+'
- submit_search_term(search)
-
- expect_issues_list_count(1)
- expect_search_term(search)
- expect(page).to have_content(issue.title)
- end
-
it 'does not show any issues' do
search = 'testing'
submit_search_term(search)
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 9702e43a559..5f7a4f26a98 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
include ActionView::Helpers::JavaScriptHelper
include ListboxHelpers
+ include ContentEditorHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -36,6 +37,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
describe 'new issue' do
before do
visit new_project_issue_path(project)
+ close_rich_text_promo_popover_if_present
end
describe 'shorten users API pagination limit' do
@@ -745,7 +747,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
context 'with the visible_label_selection_on_metadata feature flag enabled', :js do
let(:visible_label_selection_on_metadata) { true }
- it 'creates project label from dropdown' do
+ it 'creates project label from dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/416585' do
find('[data-testid="labels-select-dropdown-contents"] button').click
wait_for_all_requests
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 665c7307231..47e9575da54 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
include CookieHelper
+ include Features::AutocompleteHelpers
+ include ContentEditorHelpers
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
@@ -31,6 +33,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
before do
sign_in(user)
visit new_project_issue_path(project)
+ close_rich_text_promo_popover_if_present
wait_for_requests
end
@@ -49,6 +52,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
sign_in(user)
set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue_to_edit)
+ close_rich_text_promo_popover_if_present
wait_for_requests
end
@@ -84,6 +88,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
before do
sign_in(user)
visit project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
wait_for_requests
end
@@ -453,12 +458,4 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
wait_for_requests
end
-
- def find_autocomplete_menu
- find('.atwho-view ul', visible: true)
- end
-
- def find_highlighted_autocomplete_item
- find('.atwho-view li.cur', visible: true)
- end
end
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index 5cabaf16960..b7a0949edce 100644
--- a/spec/features/issues/markdown_toolbar_spec.rb
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Issue markdown toolbar', :js, feature_category: :team_planning do
+ include ContentEditorHelpers
+
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -11,6 +13,7 @@ RSpec.describe 'Issue markdown toolbar', :js, feature_category: :team_planning d
sign_in(user)
visit project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
end
it "doesn't include first new line when adding bold" do
@@ -32,4 +35,17 @@ RSpec.describe 'Issue markdown toolbar', :js, feature_category: :team_planning d
expect(find_field('Comment').value).to eq("test\n_underline_\n")
end
+
+ it "makes sure bold works fine after preview" do
+ fill_in 'Comment', with: "test"
+
+ click_button 'Preview'
+ click_button 'Continue editing'
+
+ page.evaluate_script('document.getElementById("note-body").setSelectionRange(0, 4)')
+
+ click_button 'Add bold text'
+
+ expect(find_field('Comment').value).to eq("**test**")
+ end
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index dae71481352..23f9347d726 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Issue notes polling', :js, feature_category: :team_planning do
include NoteInteractionHelpers
+ include ContentEditorHelpers
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
@@ -11,6 +12,7 @@ RSpec.describe 'Issue notes polling', :js, feature_category: :team_planning do
describe 'creates' do
before do
visit project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
end
it 'displays the new comment' do
@@ -31,6 +33,7 @@ RSpec.describe 'Issue notes polling', :js, feature_category: :team_planning do
before do
sign_in(user)
visit project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
end
it 'displays the updated content' do
@@ -59,7 +62,10 @@ RSpec.describe 'Issue notes polling', :js, feature_category: :team_planning do
update_note(existing_note, updated_text)
+ expect(page).to have_selector(".alert")
+
find("#note_#{existing_note.id} .note-edit-cancel").click
+ click_button('Cancel editing')
expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
end
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
index 8d6262efa53..62855c7467f 100644
--- a/spec/features/issues/notes_on_issues_spec.rb
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -3,9 +3,12 @@
require 'spec_helper'
RSpec.describe 'Create notes on issues', :js, feature_category: :team_planning do
+ include ContentEditorHelpers
+
let(:user) { create(:user) }
def submit_comment(text)
+ close_rich_text_promo_popover_if_present
fill_in 'note[note]', with: text
click_button 'Comment'
wait_for_requests
diff --git a/spec/features/issues/related_issues_spec.rb b/spec/features/issues/related_issues_spec.rb
index f460b4b1c7f..5102eeb2511 100644
--- a/spec/features/issues/related_issues_spec.rb
+++ b/spec/features/issues/related_issues_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe 'Related issues', :js, feature_category: :team_planning do
let_it_be(:private_issue) { create(:issue, project: private_project) }
let_it_be(:public_issue) { create(:issue, project: public_project) }
+ before do
+ stub_feature_flags(move_close_into_dropdown: false)
+ end
+
context 'widget visibility' do
context 'when not logged in' do
it 'does not show widget when internal project' do
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index 0cadeb62fa2..923967c52c0 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :team_planning do
+RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :service_desk do
let(:project) { create(:project, :private, service_desk_enabled: true) }
let_it_be(:user) { create(:user) }
@@ -15,6 +15,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :team_planni
project.add_maintainer(user)
sign_in(user)
+ stub_feature_flags(service_desk_vue_list: false)
end
describe 'navigation to service desk' do
@@ -176,5 +177,69 @@ RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :team_planni
end
end
end
+
+ context 'when service_desk_vue_list feature flag is enabled' do
+ before do
+ stub_feature_flags(service_desk_vue_list: true)
+ stub_feature_flags(frontend_caching: true)
+ end
+
+ context 'when there are issues' do
+ let_it_be(:project) { create(:project, :private, service_desk_enabled: true) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:service_desk_issue) { create(:issue, project: project, title: 'Help from email', author: support_bot, service_desk_reply_to: 'service.desk@example.com') }
+ let_it_be(:other_user_issue) { create(:issue, project: project, author: other_user) }
+
+ describe 'service desk info content' do
+ before do
+ visit service_desk_project_issues_path(project)
+ end
+
+ it 'displays the small info box, documentation, a button to configure service desk, and the address' do
+ aggregate_failures do
+ expect(page).to have_link('Learn more', href: help_page_path('user/project/service_desk'))
+ expect(page).not_to have_link('Enable Service Desk')
+ expect(page).to have_content(project.service_desk_address)
+ end
+ end
+ end
+
+ describe 'issues list' do
+ before do
+ visit service_desk_project_issues_path(project)
+ end
+
+ it 'only displays issues created by support bot' do
+ expect(page).to have_selector('.issues-list .issue', count: 1)
+ expect(page).to have_text('Help from email')
+ expect(page).not_to have_text('Unrelated issue')
+ end
+
+ it 'shows service_desk_reply_to in issues list' do
+ expect(page).to have_text('by GitLab Support Bot')
+ end
+ end
+ end
+ end
+
+ context 'for feature flags' do
+ let(:service_desk_issue) { create(:issue, project: project, author: support_bot, service_desk_reply_to: 'service.desk@example.com') }
+
+ before do
+ visit project_issue_path(project, service_desk_issue)
+ end
+
+ it 'pushes the service_desk_ticket feature flag to frontend when available' do
+ stub_feature_flags(service_desk_ticket: true)
+
+ expect(page).to have_pushed_frontend_feature_flags(serviceDeskTicket: true)
+ end
+
+ it 'does not push the service_desk_ticket feature flag to frontend when not available' do
+ stub_feature_flags(service_desk_ticket: false)
+
+ expect(page).not_to have_pushed_frontend_feature_flags(serviceDeskTicket: false)
+ end
+ end
end
end
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index 3ace560fb40..f18992325d8 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -3,7 +3,9 @@
require "spec_helper"
RSpec.describe "User comments on issue", :js, feature_category: :team_planning do
+ include Features::AutocompleteHelpers
include Features::NotesHelpers
+ include ContentEditorHelpers
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
@@ -14,6 +16,7 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
sign_in(user)
visit(project_issue_path(project, issue))
+ close_rich_text_promo_popover_if_present
end
context "when adding comments" do
@@ -92,10 +95,4 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
end
end
end
-
- private
-
- def find_highlighted_autocomplete_item
- find('.atwho-view li.cur', visible: true)
- end
end
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 1050bc2456f..ecb899a7ca2 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
@@ -137,13 +137,25 @@ RSpec.describe 'User creates branch and merge request on issue page', :js, featu
context "when there is a referenced merge request" do
let!(:note) do
- create(:note, :on_issue, :system, project: project, noteable: issue,
- note: "mentioned in #{referenced_mr.to_reference}")
+ create(
+ :note,
+ :on_issue,
+ :system,
+ project: project,
+ noteable: issue,
+ note: "mentioned in #{referenced_mr.to_reference}"
+ )
end
let(:referenced_mr) do
- create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes #{issue.to_reference}", author: user)
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ target_project: project,
+ description: "Fixes #{issue.to_reference}",
+ author: user
+ )
end
before do
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index d4148717f0a..76b07d903bc 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -4,6 +4,7 @@ require "spec_helper"
RSpec.describe "User creates issue", feature_category: :team_planning do
include DropzoneHelper
+ include ContentEditorHelpers
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
@@ -41,6 +42,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
sign_in(user)
visit(new_project_issue_path(project))
+ close_rich_text_promo_popover_if_present
end
context 'available metadata' do
@@ -159,7 +161,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
click_button 'Create issue'
page.within '.issuable-sidebar' do
- expect(page).to have_content date.to_s(:medium)
+ expect(page).to have_content date.to_fs(:medium)
end
end
end
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index bc20660d2a0..0938f9c7d12 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -4,6 +4,7 @@ require "spec_helper"
RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do
include CookieHelper
+ include ContentEditorHelpers
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) }
@@ -27,6 +28,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
before do
stub_licensed_features(multiple_issue_assignees: false)
visit edit_project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
end
it_behaves_like 'edits content using the content editor'
@@ -82,7 +84,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
click_button _('Save changes')
page.within '.issuable-sidebar' do
- expect(page).to have_content date.to_s(:medium)
+ expect(page).to have_content date.to_fs(:medium)
end
end
@@ -125,7 +127,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
expect(issuable_form).to have_selector(markdown_field_focused_selector)
page.within issuable_form do
- click_button("Switch to rich text")
+ click_button("Switch to rich text editing")
end
expect(issuable_form).not_to have_selector(content_editor_focused_selector)
@@ -137,7 +139,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
expect(issuable_form).to have_selector(content_editor_focused_selector)
page.within issuable_form do
- click_button("Switch to Markdown")
+ click_button("Switch to plain text editing")
end
expect(issuable_form).not_to have_selector(markdown_field_focused_selector)
diff --git a/spec/features/issues/user_filters_issues_spec.rb b/spec/features/issues/user_filters_issues_spec.rb
index 9f69e94b86c..593b43698a2 100644
--- a/spec/features/issues/user_filters_issues_spec.rb
+++ b/spec/features/issues/user_filters_issues_spec.rb
@@ -8,11 +8,13 @@ RSpec.describe 'User filters issues', :js, feature_category: :team_planning do
before do
%w[foobar barbaz].each do |title|
- create(:issue,
- author: user,
- assignees: [user],
- project: project,
- title: title)
+ create(
+ :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ title: title
+ )
end
@issue = Issue.find_by(title: 'foobar')
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index 539e429534e..e1099ba242e 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'User interacts with awards', feature_category: :team_planning do
include MobileHelpers
+ include ContentEditorHelpers
let(:user) { create(:user) }
@@ -16,6 +17,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
sign_in(user)
visit(project_issue_path(project, issue))
+ close_rich_text_promo_popover_if_present
end
it 'toggles the thumbsup award emoji', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27959' do
diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
index 91b18454af5..ef448c06a3f 100644
--- a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
+++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_category: :team_planning do
+ include ContentEditorHelpers
+
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -20,6 +22,7 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
using_session :other_session do
visit project_issue_path(project, issue)
+ close_rich_text_promo_popover_if_present
expect(page.find('.assignee')).to have_content 'None'
end
@@ -43,6 +46,7 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
using_session :other_session do
visit project_issue_path(project, issue)
wait_for_requests
+ close_rich_text_promo_popover_if_present
expect(labels_value).to have_content('None')
end
@@ -50,6 +54,7 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
visit project_issue_path(project, issue)
wait_for_requests
+ close_rich_text_promo_popover_if_present
expect(labels_value).to have_content('None')
page.within(labels_widget) do
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index e85a521e242..dc149ccc698 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -9,6 +9,7 @@ require 'spec_helper'
# for each existing quick action unless they test something not tested by existing tests.
RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_planning do
include Features::NotesHelpers
+ include ContentEditorHelpers
context "issuable common quick actions" do
let(:new_url_opts) { {} }
@@ -34,6 +35,7 @@ RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_
sign_in(user)
visit project_issue_path(project, issue)
wait_for_all_requests
+ close_rich_text_promo_popover_if_present
end
after do
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index e8f40a1ceab..eb79d6e64f3 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
include FilteredSearchHelpers
+ include ContentEditorHelpers
let!(:user) { create(:user) }
let!(:grandparent) { create(:group) }
@@ -165,6 +166,7 @@ RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
context 'when creating new issuable' do
before do
visit new_project_issue_path(project_1)
+ close_rich_text_promo_popover_if_present
end
it 'is able to assign ancestor group labels' do
@@ -202,6 +204,7 @@ RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
context 'when creating new issuable' do
before do
visit new_project_issue_path(project_1)
+ close_rich_text_promo_popover_if_present
end
it 'is able to assign ancestor group labels' do
@@ -233,6 +236,7 @@ RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
project_1.add_developer(user)
visit project_issue_path(project_1, issue)
+ close_rich_text_promo_popover_if_present
end
it_behaves_like 'assigning labels from sidebar'
diff --git a/spec/features/markdown/keyboard_shortcuts_spec.rb b/spec/features/markdown/keyboard_shortcuts_spec.rb
index cfb8e61689f..6f128e16041 100644
--- a/spec/features/markdown/keyboard_shortcuts_spec.rb
+++ b/spec/features/markdown/keyboard_shortcuts_spec.rb
@@ -102,15 +102,16 @@ RSpec.describe 'Markdown keyboard shortcuts', :js, feature_category: :team_plann
it_behaves_like 'keyboard shortcuts'
it_behaves_like 'no side effects'
- end
- context 'Haml markdown editor' do
- let(:path_to_visit) { new_project_issue_path(project) }
- let(:markdown_field) { find_field('Description') }
- let(:non_markdown_field) { find_field('Title') }
+ context 'if preview is toggled before shortcuts' do
+ before do
+ click_button "Preview"
+ click_button "Continue editing"
+ end
- it_behaves_like 'keyboard shortcuts'
- it_behaves_like 'no side effects'
+ it_behaves_like 'keyboard shortcuts'
+ it_behaves_like 'no side effects'
+ end
end
def type_and_select(text)
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index c9aa22e396b..7603696c60c 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork', :js, :sidekiq_might_not_need_inline,
-feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
include Features::SourceEditorSpecHelpers
include ProjectForksHelper
let(:user) { create(:user, username: 'the-maintainer') }
@@ -12,13 +12,15 @@ feature_category: :code_review_workflow do
let(:source_project) { fork_project(target_project, author, repository: true) }
let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: target_project,
- source_branch: 'fix',
- target_branch: 'master',
- author: author,
- allow_collaboration: true)
+ create(
+ :merge_request,
+ source_project: source_project,
+ target_project: target_project,
+ source_branch: 'fix',
+ target_branch: 'master',
+ author: author,
+ allow_collaboration: true
+ )
end
before do
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index e3989a8a192..38291573256 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inline, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
@@ -15,6 +17,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
context 'presents merged merge request content' do
it 'when merge method is set to merge commit' do
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
click_merge_button
@@ -30,6 +33,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
merge_request = create(:merge_request, :rebased, source_project: project)
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
click_merge_button
@@ -40,6 +44,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
merge_request = create(:merge_request, :rebased, source_project: project, squash: true)
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
click_merge_button
@@ -51,6 +56,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
context 'with removing the source branch' do
before do
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
end
it 'accepts a merge request' do
@@ -69,6 +75,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
context 'without removing the source branch' do
before do
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
end
it 'accepts a merge request' do
@@ -86,6 +93,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
context 'when a URL has an anchor' do
before do
visit(merge_request_path(merge_request, anchor: 'note_123'))
+ close_rich_text_promo_popover_if_present
end
it 'accepts a merge request' do
@@ -106,6 +114,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
merge_request.mark_as_mergeable
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
end
it 'accepts a merge request' do
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index 0ff773ef02d..149b2e2bb0f 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'create a merge request, allowing commits from members who can merge to the target branch', :js,
-feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public, :repository) }
@@ -67,10 +67,12 @@ feature_category: :code_review_workflow do
context 'when a member who can merge tries to edit the option' do
let(:member) { create(:user) }
let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: target_project,
- source_branch: 'fixes')
+ create(
+ :merge_request,
+ source_project: source_project,
+ target_project: target_project,
+ source_branch: 'fixes'
+ )
end
before do
diff --git a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
index 537702df12d..446f6a470de 100644
--- a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
+++ b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User closes/reopens a merge request', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500',
- feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index 35e2fa2f89c..215fe1f7521 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_workflow do
include MergeRequestDiffHelpers
include RepoHelpers
+ include ContentEditorHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) do
@@ -128,6 +129,30 @@ RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_wo
context 'when adding comments' do
include_examples 'comment on merge request file'
+
+ context 'when adding a diff suggestion in rich text editor' do
+ it 'works on the Overview tab' do
+ click_diff_line(find_by_scrolling("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: "```suggestion:-0+0\nchanged line\n```")
+ find('.js-comment-button').click
+ end
+
+ visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
+
+ page.within('.notes .discussion') do
+ find('.js-vue-discussion-reply').click
+ click_button "Switch to rich text editing"
+ click_button "Insert suggestion"
+ end
+
+ within '[data-testid="content-editor"]' do
+ expect(page).to have_content('Suggested change From line')
+ end
+ end
+ end
end
context 'when adding multiline comments' do
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index 97b423f2cc2..eab5cee976e 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -110,11 +110,13 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
context 'when project is public and merge requests are private' do
let_it_be(:project) do
- create(:project,
- :public,
- :repository,
- group: group,
- merge_requests_access_level: ProjectFeature::DISABLED)
+ create(
+ :project,
+ :public,
+ :repository,
+ group: group,
+ merge_requests_access_level: ProjectFeature::DISABLED
+ )
end
context 'and user is a guest' do
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 fa713bdbc5d..2fcbb4e70c3 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -32,6 +32,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
end
let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['title'] || '' }
+ let(:sidebar_assignee_merge_ability) { sidebar_assignee_avatar_link['data-cannot-merge'] || '' }
context 'when GraphQL assignees widget feature flag is disabled' do
let(:sidebar_assignee_dropdown_item) do
@@ -57,13 +58,13 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
wait_for_requests
end
- shared_examples 'when assigned' do |expected_tooltip: ''|
+ shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
it 'shows assignee name' do
expect(sidebar_assignee_block).to have_text(assignee.name)
end
- it "shows assignee tooltip '#{expected_tooltip}'" do
- expect(sidebar_assignee_tooltip).to eql(expected_tooltip)
+ it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
+ expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
end
context 'when edit is clicked' do
@@ -88,7 +89,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
context 'when assigned to developer' do
let(:assignee) { project_developers.last }
- it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge'
+ it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
end
end
@@ -140,13 +141,13 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
wait_for_requests
end
- shared_examples 'when assigned' do |expected_tooltip: ''|
+ shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''|
it 'shows assignee name' do
expect(sidebar_assignee_block).to have_text(assignee.name)
end
- it "shows assignee tooltip '#{expected_tooltip}'" do
- expect(sidebar_assignee_tooltip).to eql(expected_tooltip)
+ it "sets data-cannot-merge to '#{expected_cannot_merge}'" do
+ expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge)
end
context 'when edit is clicked' do
@@ -169,7 +170,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
context 'when assigned to developer' do
let(:assignee) { project_developers.last }
- it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge'
+ it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true'
end
end
diff --git a/spec/features/merge_request/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb
index 584a17ae33d..b1cff72c374 100644
--- a/spec/features/merge_request/user_edits_merge_request_spec.rb
+++ b/spec/features/merge_request/user_edits_merge_request_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User edits a merge request', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -85,6 +87,8 @@ RSpec.describe 'User edits a merge request', :js, feature_category: :code_review
describe 'changing target branch' do
it 'allows user to change target branch' do
+ close_rich_text_promo_popover_if_present
+
expect(page).to have_content('From master into feature')
first('.js-target-branch').click
diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb
index 76588832ee1..ab7183775b9 100644
--- a/spec/features/merge_request/user_edits_mr_spec.rb
+++ b/spec/features/merge_request/user_edits_mr_spec.rb
@@ -184,7 +184,11 @@ RSpec.describe 'Merge request > User edits MR', feature_category: :code_review_w
it 'allows to unselect "Remove source branch"', :js do
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
- visit edit_project_merge_request_path(target_project, merge_request)
+ begin
+ visit edit_project_merge_request_path(target_project, merge_request)
+ rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError
+ end
+
uncheck 'Delete source branch when merge request is accepted'
click_button 'Save changes'
diff --git a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
index a013666a496..a96ec1f68aa 100644
--- a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
+++ b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe 'Batch diffs', :js, feature_category: :code_review_workflow do
context 'which is in at least page 2 of the batched pages of diffs' do
it 'scrolls to the correct discussion',
- quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293814' } do
+ quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293814' } do
page.within get_first_diff do
click_link('just now')
end
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index d47968ebc6b..5fe9947d0df 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -3,20 +3,28 @@
require 'spec_helper'
RSpec.describe 'Merge requests > User merges immediately', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let!(:merge_request) do
- create(:merge_request_with_diffs, source_project: project,
- author: user,
- title: 'Bug NS-04',
- head_pipeline: pipeline,
- source_branch: pipeline.ref)
+ create(
+ :merge_request_with_diffs,
+ source_project: project,
+ author: user,
+ title: 'Bug NS-04',
+ head_pipeline: pipeline,
+ source_branch: pipeline.ref
+ )
end
let(:pipeline) do
- create(:ci_pipeline, project: project,
- ref: 'master',
- sha: project.repository.commit('master').id)
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.repository.commit('master').id
+ )
end
context 'when there is active pipeline for merge request' do
@@ -25,6 +33,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js, feature_category
project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
it 'enables merge immediately' do
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index 6ffb33603d5..402405e1fb6 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe "User merges a merge request", :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:user) { project.first_owner }
before do
@@ -29,6 +31,7 @@ RSpec.describe "User merges a merge request", :js, feature_category: :code_revie
create(:merge_request, source_project: project, source_branch: 'branch-1')
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
expect(page).to have_css('.js-merge-counter', text: '2')
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index 19b5ad0fa84..62404077cea 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
@@ -13,22 +15,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
context 'project does not have CI enabled' do
it 'allows MR to be merged' do
- stub_feature_flags(auto_merge_labels_mr_widget: false)
-
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_button 'Merge'
- end
- end
- end
-
- context 'project does not have CI enabled and auto_merge_labels_mr_widget on' do
- it 'allows MR to be merged' do
- stub_feature_flags(auto_merge_labels_mr_widget: true)
-
visit project_merge_request_path(project, merge_request)
wait_for_requests
@@ -41,89 +27,19 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
context 'when project has CI enabled' do
let!(:pipeline) do
- create(:ci_empty_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- status: status, head_pipeline_of: merge_request)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: status,
+ head_pipeline_of: merge_request
+ )
end
context 'when merge requests can only be merged if the pipeline succeeds' do
before do
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
-
- stub_feature_flags(auto_merge_labels_mr_widget: false)
- end
-
- context 'when CI is running' do
- let(:status) { :running }
-
- it 'does not allow to merge immediately' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).to have_button 'Merge when pipeline succeeds'
- expect(page).not_to have_button '.js-merge-moment'
- end
- end
-
- context 'when CI failed' do
- let(:status) { :failed }
-
- it 'does not allow MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).not_to have_button('Merge', exact: true)
- expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
- end
- end
-
- context 'when CI canceled' do
- let(:status) { :canceled }
-
- it 'does not allow MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).not_to have_button('Merge', exact: true)
- expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
- end
- end
-
- context 'when CI succeeded' do
- let(:status) { :success }
-
- it 'allows MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).to have_button('Merge', exact: true)
- end
- end
-
- context 'when CI skipped' do
- let(:status) { :skipped }
-
- it 'does not allow MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).not_to have_button('Merge', exact: true)
- end
- end
- end
-
- context 'when merge requests can only be merged if the pipeline succeeds with auto_merge_labels_mr_widget on' do
- before do
- project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
-
- stub_feature_flags(auto_merge_labels_mr_widget: true)
end
context 'when CI is running' do
@@ -193,58 +109,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
context 'when merge requests can be merged when the build failed' do
before do
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
-
- stub_feature_flags(auto_merge_labels_mr_widget: false)
- end
-
- context 'when CI is running' do
- let(:status) { :running }
-
- it 'allows MR to be merged immediately' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- expect(page).to have_button 'Merge when pipeline succeeds'
-
- page.find('.js-merge-moment').click
- expect(page).to have_content 'Merge immediately'
- end
- end
-
- context 'when CI failed' do
- let(:status) { :failed }
-
- it 'allows MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
- page.within('.mr-state-widget') do
- expect(page).to have_button 'Merge'
- end
- end
- end
-
- context 'when CI succeeded' do
- let(:status) { :success }
-
- it 'allows MR to be merged' do
- visit project_merge_request_path(project, merge_request)
-
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_button 'Merge'
- end
- end
- end
- end
-
- context 'when merge requests can be merged when the build failed with auto_merge_labels_mr_widget on' do
- before do
- project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
-
- stub_feature_flags(auto_merge_labels_mr_widget: true)
end
context 'when CI is running' do
@@ -252,6 +116,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
it 'allows MR to be merged immediately' do
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
wait_for_requests
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index e42e4735ee2..ebec8a6d2ea 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -6,17 +6,23 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) do
- create(:merge_request_with_diffs, source_project: project,
- author: user,
- title: 'Bug NS-04',
- merge_params: { force_remove_source_branch: '1' })
+ create(
+ :merge_request_with_diffs,
+ source_project: project,
+ author: user,
+ title: 'Bug NS-04',
+ merge_params: { force_remove_source_branch: '1' }
+ )
end
let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- head_pipeline_of: merge_request)
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request
+ )
end
before do
@@ -26,83 +32,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur
context 'when there is active pipeline for merge request' do
before do
create(:ci_build, pipeline: pipeline)
- stub_feature_flags(auto_merge_labels_mr_widget: false)
-
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- describe 'enabling Merge when pipeline succeeds' do
- shared_examples 'Merge when pipeline succeeds activator' do
- it 'activates the Merge when pipeline succeeds feature', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410105' do
- click_button "Merge when pipeline succeeds"
-
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
- expect(page).to have_content "Source branch will not be deleted"
- expect(page).to have_selector ".js-cancel-auto-merge"
- visit project_merge_request_path(project, merge_request) # Needed to refresh the page
- expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
- end
- end
-
- context "when enabled immediately" do
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
-
- context 'when enabled after pipeline status changed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/258667' do
- before do
- pipeline.run!
-
- # We depend on merge request widget being reloaded
- # so we have to wait for asynchronous call to reload it
- # and have_content expectation handles that.
- #
- expect(page).to have_content "Pipeline ##{pipeline.id} running"
- end
-
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
-
- context 'when enabled after it was previously canceled' do
- before do
- click_button "Merge when pipeline succeeds"
-
- wait_for_requests
-
- click_button "Cancel auto-merge"
-
- wait_for_requests
-
- expect(page).to have_content 'Merge when pipeline succeeds'
- end
-
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
-
- context 'when it was enabled and then canceled' do
- let(:merge_request) do
- create(:merge_request_with_diffs,
- :merge_when_pipeline_succeeds,
- source_project: project,
- title: 'Bug NS-04',
- author: user,
- merge_user: user)
- end
-
- before do
- merge_request.merge_params['force_remove_source_branch'] = '0'
- merge_request.save!
- click_button "Cancel auto-merge"
- end
-
- it_behaves_like 'Merge when pipeline succeeds activator'
- end
- end
- end
-
- context 'when there is active pipeline for merge request with auto_merge_labels_mr_widget on' do
- before do
- create(:ci_build, pipeline: pipeline)
stub_feature_flags(auto_merge_labels_mr_widget: true)
sign_in(user)
@@ -144,12 +73,14 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur
context 'when it was enabled and then canceled' do
let(:merge_request) do
- create(:merge_request_with_diffs,
- :merge_when_pipeline_succeeds,
- source_project: project,
- title: 'Bug NS-04',
- author: user,
- merge_user: user)
+ create(
+ :merge_request_with_diffs,
+ :merge_when_pipeline_succeeds,
+ source_project: project,
+ title: 'Bug NS-04',
+ author: user,
+ merge_user: user
+ )
end
before do
@@ -165,91 +96,15 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, featur
context 'when merge when pipeline succeeds is enabled' do
let(:merge_request) do
- create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
+ create(
+ :merge_request_with_diffs,
+ :simple,
+ :merge_when_pipeline_succeeds,
source_project: project,
author: user,
merge_user: user,
- title: 'MepMep')
- end
-
- let!(:build) do
- create(:ci_build, pipeline: pipeline)
- end
-
- before do
- stub_feature_flags(auto_merge_labels_mr_widget: false)
- sign_in user
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'allows to cancel the automatic merge', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/412416' do
- click_button "Cancel auto-merge"
-
- expect(page).to have_button "Merge when pipeline succeeds"
-
- refresh
-
- expect(page).to have_content "canceled the automatic merge"
- end
-
- context 'when pipeline succeeds' do
- before do
- build.success
- refresh
- end
-
- it 'merges merge request', :sidekiq_might_not_need_inline do
- expect(page).to have_content 'Changes merged'
- expect(merge_request.reload).to be_merged
- end
- end
-
- context 'view merge request with MWPS enabled but automatically merge fails' do
- before do
- merge_request.update!(
- merge_user: merge_request.author,
- merge_error: 'Something went wrong'
- )
- refresh
- end
-
- it 'shows information about the merge error' do
- # Wait for the `ci_status` and `merge_check` requests
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_content('Something went wrong. Try again.')
- end
- end
- end
-
- context 'view merge request with MWPS enabled but automatically merge fails' do
- before do
- merge_request.update!(
- merge_user: merge_request.author,
- merge_error: 'Something went wrong.'
- )
- refresh
- end
-
- it 'shows information about the merge error' do
- # Wait for the `ci_status` and `merge_check` requests
- wait_for_requests
-
- page.within('.mr-state-widget') do
- expect(page).to have_content('Something went wrong. Try again.')
- end
- end
- end
- end
-
- context 'when merge when pipeline succeeds is enabled and auto_merge_labels_mr_widget on' do
- let(:merge_request) do
- create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
- source_project: project,
- author: user,
- merge_user: user,
- title: 'MepMep')
+ title: 'MepMep'
+ )
end
let!(:build) do
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 601310cbacf..63f03ae64e0 100644
--- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
@@ -20,13 +20,15 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
let(:source_project) { fork_project(project, author, repository: true) }
let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'master',
- author: author,
- allow_collaboration: true)
+ create(
+ :merge_request,
+ source_project: source_project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master',
+ author: author,
+ allow_collaboration: true
+ )
end
it 'shows instructions' do
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index a749821b083..0278d2af08f 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_review_workflow do
include NoteInteractionHelpers
+ include ContentEditorHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -13,8 +14,7 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_
end
let!(:note) do
- create(:note_on_merge_request, :with_attachment, noteable: merge_request,
- project: project)
+ create(:note_on_merge_request, :with_attachment, noteable: merge_request, project: project)
end
before do
@@ -22,6 +22,7 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_
sign_in(user)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
subject { page }
@@ -47,8 +48,8 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_
it 'has enable submit button, preview button and saves content to local storage' do
page.within('.js-main-target-form') do
page.within('[data-testid="comment-button"]') do
- expect(page).to have_css('.split-content-button')
- expect(page).not_to have_css('.split-content-button[disabled]')
+ expect(page).to have_css('.gl-button')
+ expect(page).not_to have_css('.disabled')
end
expect(page).to have_css('.js-md-preview-button', visible: true)
end
@@ -131,16 +132,16 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_
describe 'when previewing a note' do
it 'shows the toolbar buttons when editing a note' do
- page.within('.js-main-target-form') do
- expect(page).to have_css('.md-header-toolbar')
+ page.within('.js-main-target-form .md-header-toolbar') do
+ expect(page).to have_css('button', count: 16)
end
end
it 'hides the toolbar buttons when previewing a note' do
wait_for_requests
click_button("Preview")
- page.within('.js-main-target-form') do
- expect(page).not_to have_css('.md-header-toolbar')
+ page.within('.js-main-target-form .md-header-toolbar') do
+ expect(page).to have_css('button', count: 1)
end
end
end
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 5da9f4a1f19..e8ffca43aa2 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User resolves diff notes and threads', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:guest) { create(:user) }
@@ -10,9 +12,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js, feat
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "| Markdown | Table |\n|-------|---------|\n| first | second |") }
let(:path) { "files/ruby/popen.rb" }
let(:position) do
- build(:text_diff_position,
- file: path, old_line: nil, new_line: 9,
- diff_refs: merge_request.diff_refs)
+ build(:text_diff_position, file: path, old_line: nil, new_line: 9, diff_refs: merge_request.diff_refs)
end
before do
@@ -543,5 +543,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js, feat
# Wait for MR widget to load
wait_for_requests
+
+ close_rich_text_promo_popover_if_present
end
end
diff --git a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
index 5c41ac79552..cb57f1fd549 100644
--- a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
+++ b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Merge request > User resolves outdated diff discussions',
- :js, feature_category: :code_review_workflow do
+ :js, feature_category: :code_review_workflow do
let(:project) { create(:project, :repository, :public) }
let(:merge_request) do
@@ -30,17 +30,21 @@ RSpec.describe 'Merge request > User resolves outdated diff discussions',
end
let!(:outdated_discussion) do
- create(:diff_note_on_merge_request,
- project: project,
- noteable: merge_request,
- position: outdated_position).to_discussion
+ create(
+ :diff_note_on_merge_request,
+ project: project,
+ noteable: merge_request,
+ position: outdated_position
+ ).to_discussion
end
let!(:current_discussion) do
- create(:diff_note_on_merge_request,
- noteable: merge_request,
- project: project,
- position: current_position).to_discussion
+ create(
+ :diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: current_position
+ ).to_discussion
end
before do
diff --git a/spec/features/merge_request/user_resolves_wip_mr_spec.rb b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
index 01cc6bd5167..15a7755429b 100644
--- a/spec/features/merge_request/user_resolves_wip_mr_spec.rb
+++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
@@ -6,17 +6,23 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) do
- create(:merge_request_with_diffs, source_project: project,
- author: user,
- title: 'Draft: Bug NS-04',
- merge_params: { force_remove_source_branch: '1' })
+ create(
+ :merge_request_with_diffs,
+ source_project: project,
+ author: user,
+ title: 'Draft: Bug NS-04',
+ merge_params: { force_remove_source_branch: '1' }
+ )
end
let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- head_pipeline_of: merge_request)
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request
+ )
end
before do
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 8c782056aa4..68adc4d47b6 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User reverts a merge request', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
@@ -13,6 +15,7 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi
sign_in(user)
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
page.within('.mr-state-widget') do
click_button 'Merge'
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 6dcebad300c..d237faba663 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js, feature_categ
wait_for_requests
assert_env_widget("Deployed to", environment.name)
- expect(find('.js-deploy-time')['title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
+ expect(find('.js-deploy-time')['title']).to eq(deployment.created_at.to_time.in_time_zone.to_fs(:medium))
end
context 'when a user created a new merge request with the same SHA' do
@@ -115,8 +115,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js, feature_categ
context 'with stop action' do
let(:manual) do
- create(:ci_build, :manual, pipeline: pipeline,
- name: 'close_app', environment: environment.name)
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app', environment: environment.name)
end
before do
@@ -146,8 +145,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js, feature_categ
context 'with stop action with the review_apps_redeploy_mr_widget feature flag turned on' do
let(:manual) do
- create(:ci_build, :manual, pipeline: pipeline,
- name: 'close_app', environment: environment.name)
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app', environment: environment.name)
end
before do
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index 3fb3ef12fcc..57f378a86b6 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -77,8 +77,9 @@ RSpec.describe 'Merge request > User sees diff', :js, feature_category: :code_re
sign_in(author_user)
visit diffs_project_merge_request_path(project, merge_request)
- # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
- expect(page).to have_selector(".js-edit-blob", visible: false)
+ first(".js-diff-more-actions").click
+
+ expect(page).to have_selector(".js-edit-blob")
end
end
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 338e4329190..4cce40972e9 100644
--- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -13,27 +13,30 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
describe 'Code discussions' do
let!(:position) do
- build(:text_diff_position, :added,
- file: "files/images/wm.svg",
- new_line: 1,
- diff_refs: merge_request.diff_refs
+ build(
+ :text_diff_position, :added,
+ file: "files/images/wm.svg",
+ new_line: 1,
+ diff_refs: merge_request.diff_refs
)
end
let!(:first_discussion) do
- create(:diff_note_on_merge_request,
- noteable: merge_request,
- project: project,
- position: position
- ).to_discussion
+ create(
+ :diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: position
+ ).to_discussion
end
let!(:second_discussion) do
- create(:diff_note_on_merge_request,
- noteable: merge_request,
- project: project,
- position: position
- ).to_discussion
+ create(
+ :diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: position
+ ).to_discussion
end
let(:first_discussion_selector) { ".discussion[data-discussion-id='#{first_discussion.id}']" }
@@ -74,11 +77,12 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
context 'with resolved threads' do
let!(:resolved_discussion) do
- create(:diff_note_on_merge_request,
- noteable: merge_request,
- project: project,
- position: position
- ).to_discussion
+ create(
+ :diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: position
+ ).to_discussion
end
let(:resolved_discussion_selector) { ".discussion[data-discussion-id='#{resolved_discussion.id}']" }
@@ -92,7 +96,7 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
end
it 'excludes resolved threads during navigation',
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383687' do
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383687' do
goto_next_thread
goto_next_thread
goto_next_thread
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index 3ca5ac23ddb..3482d468bc1 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees threads', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -29,6 +31,7 @@ RSpec.describe 'Merge request > User sees threads', :js, feature_category: :code
before do
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
context 'active threads' do
@@ -71,6 +74,7 @@ RSpec.describe 'Merge request > User sees threads', :js, feature_category: :code
before do
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
# TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
index 476be5ab599..9955c13b769 100644
--- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees merge button depending on unresolved threads', :js,
-feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index ca12e0e2b65..8c4dbf5ebfd 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -36,11 +36,13 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when a user created a merge request in the parent project' do
let!(:merge_request) do
- create(:merge_request,
- source_project: project,
- target_project: project,
- source_branch: 'feature',
- target_branch: 'master')
+ create(
+ :merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'feature',
+ target_branch: 'master'
+ )
end
let!(:push_pipeline) do
@@ -56,8 +58,6 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
before do
- stub_feature_flags(auto_merge_labels_mr_widget: false)
-
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
@@ -144,53 +144,8 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
end
- context 'when a user merges a merge request in the parent project', :sidekiq_might_not_need_inline do
- before do
- click_link 'Overview'
- click_button 'Merge when pipeline succeeds'
-
- wait_for_requests
- end
-
- context 'when detached merge request pipeline is pending' do
- it 'waits the head pipeline' do
- expect(page).to have_content('to be merged automatically when the pipeline succeeds')
- expect(page).to have_button('Cancel auto-merge')
- end
- end
-
- context 'when detached merge request pipeline succeeds' do
- before do
- detached_merge_request_pipeline.reload.succeed!
-
- wait_for_requests
- end
-
- it 'merges the merge request' do
- expect(page).to have_content('Merged by')
- expect(page).to have_button('Revert')
- end
- end
-
- context 'when branch pipeline succeeds' do
- before do
- click_link 'Overview'
- push_pipeline.reload.succeed!
-
- wait_for_requests
- end
-
- it 'waits the head pipeline' do
- expect(page).to have_content('to be merged automatically when the pipeline succeeds')
- expect(page).to have_button('Cancel auto-merge')
- end
- end
- end
-
- context 'when a user created a merge request in the parent project with auto_merge_labels_mr_widget on' do
+ context 'when a user created a merge request in the parent project' do
before do
- stub_feature_flags(auto_merge_labels_mr_widget: true)
-
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
@@ -263,11 +218,13 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when a user created a merge request from a forked project to the parent project', :sidekiq_might_not_need_inline do
let(:merge_request) do
- create(:merge_request,
- source_project: forked_project,
- target_project: project,
- source_branch: 'feature',
- target_branch: 'master')
+ create(
+ :merge_request,
+ source_project: forked_project,
+ target_project: project,
+ source_branch: 'feature',
+ target_branch: 'master'
+ )
end
let!(:push_pipeline) do
@@ -390,8 +347,15 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when the latest pipeline is running in the parent project' do
before do
- Ci::CreatePipelineService.new(project, user, ref: 'feature')
- .execute(:merge_request_event, merge_request: merge_request)
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ user: user,
+ merge_request: merge_request,
+ status: :running)
+ merge_request.update_head_pipeline
end
context 'when the previous pipeline failed in the fork project' do
@@ -404,10 +368,10 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
project.update!(only_allow_merge_if_pipeline_succeeds: true)
end
- it 'shows MWPS button' do
+ it 'shows Set to auto-merge button' do
visit project_merge_request_path(project, merge_request)
- expect(page).to have_button('Merge when pipeline succeeds')
+ expect(page).to have_button('Set to auto-merge')
end
end
end
@@ -417,7 +381,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
before do
click_link("Overview")
- click_button 'Merge when pipeline succeeds'
+ click_button 'Set to auto-merge'
wait_for_requests
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index cb56e79fcc0..3cac24838a3 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
include ProjectForksHelper
include TestReportsHelper
include ReactiveCachingHelpers
+ include ContentEditorHelpers
let(:project) { create(:project, :repository) }
let(:project_only_mwps) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
@@ -57,6 +58,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
merge_request.update!(head_pipeline: pipeline)
deployment.update!(status: :success)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
it 'shows environments link' do
@@ -118,15 +120,19 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
context 'view merge request with failed GitLab CI pipelines' do
before do
commit_status = create(:commit_status, project: project, status: 'failed')
- pipeline = create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- status: 'failed',
- statuses: [commit_status],
- head_pipeline_of: merge_request)
+ pipeline = create(
+ :ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: 'failed',
+ statuses: [commit_status],
+ head_pipeline_of: merge_request
+ )
create(:ci_build, :pending, pipeline: pipeline)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
it 'has merge button that shows modal when pipeline does not succeeded' do
@@ -278,12 +284,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
context 'view merge request with MWBS button' do
before do
commit_status = create(:commit_status, project: project, status: 'pending')
- pipeline = create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- status: 'pending',
- statuses: [commit_status],
- head_pipeline_of: merge_request)
+ pipeline = create(
+ :ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ status: 'pending',
+ statuses: [commit_status],
+ head_pipeline_of: merge_request
+ )
create(:ci_build, :pending, pipeline: pipeline)
visit project_merge_request_path(project, merge_request)
@@ -298,9 +307,12 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
context 'view merge request where there is no pipeline yet' do
before do
- pipeline = create(:ci_pipeline, project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch)
+ pipeline = create(
+ :ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch
+ )
create(:ci_build, pipeline: pipeline)
visit project_merge_request_path(project, merge_request)
@@ -394,6 +406,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
before do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
it 'updates the MR widget', :sidekiq_might_not_need_inline do
@@ -415,6 +428,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
sign_in(user2)
merge_request.update!(source_project: forked_project)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
end
it 'user can merge into the target project', :sidekiq_inline do
@@ -452,6 +466,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
allow_any_instance_of(MergeRequest).to receive(:merge_ongoing?).and_return(true)
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
wait_for_requests
@@ -510,11 +525,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
context 'when merge request has test reports' do
let!(:head_pipeline) do
- create(:ci_pipeline,
- :success,
- project: project,
- ref: merge_request.source_branch,
- sha: merge_request.diff_head_sha)
+ create(
+ :ci_pipeline,
+ :success,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
end
let!(:build) { create(:ci_build, :success, pipeline: head_pipeline, project: project) }
diff --git a/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb b/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb
index fac0a84f155..7b8ac50f1ae 100644
--- a/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb
@@ -3,16 +3,19 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees MR from deleted forked project',
- :js, feature_category: :code_review_workflow do
+ :js, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:forked_project) { fork_project(project, nil, repository: true) }
let!(:merge_request) do
- create(:merge_request_with_diffs, source_project: forked_project,
- target_project: project,
- description: 'Test merge request')
+ create(
+ :merge_request_with_diffs,
+ source_project: forked_project,
+ target_project: project,
+ description: 'Test merge request'
+ )
end
before do
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index 9b46cf37648..29a76768774 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -6,7 +6,7 @@ require 'spec_helper'
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove ":js".
RSpec.describe 'Merge request > User sees MR with deleted source branch',
- :js, feature_category: :code_review_workflow do
+ :js, feature_category: :code_review_workflow do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
index ac195dd9873..ad7ed1ceada 100644
--- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
@@ -4,25 +4,33 @@ require 'spec_helper'
RSpec.describe 'Merge request > User sees notes from forked project', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
+ include ContentEditorHelpers
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:forked_project) { fork_project(project, nil, repository: true) }
let!(:merge_request) do
- create(:merge_request_with_diffs, source_project: forked_project,
- target_project: project,
- description: 'Test merge request')
+ create(
+ :merge_request_with_diffs,
+ source_project: forked_project,
+ target_project: project,
+ description: 'Test merge request'
+ )
end
before do
- create(:note_on_commit, note: 'A commit comment',
- project: forked_project,
- commit_id: merge_request.commit_shas.first)
+ create(
+ :note_on_commit,
+ note: 'A commit comment',
+ project: forked_project,
+ commit_id: merge_request.commit_shas.first
+ )
sign_in(user)
end
it 'user can reply to the comment', :sidekiq_might_not_need_inline do
visit project_merge_request_path(project, merge_request)
+ close_rich_text_promo_popover_if_present
expect(page).to have_content('A commit comment')
diff --git a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
index 0816b14f9a5..5801e8a1a11 100644
--- a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
@@ -3,23 +3,28 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees pipelines from forked project', :js,
-feature_category: :continuous_integration do
+ feature_category: :continuous_integration do
include ProjectForksHelper
let(:target_project) { create(:project, :public, :repository) }
let(:user) { target_project.creator }
let(:forked_project) { fork_project(target_project, nil, repository: true) }
let!(:merge_request) do
- create(:merge_request_with_diffs, source_project: forked_project,
- target_project: target_project,
- description: 'Test merge request')
+ create(
+ :merge_request_with_diffs,
+ source_project: forked_project,
+ target_project: target_project,
+ description: 'Test merge request'
+ )
end
let(:pipeline) do
- create(:ci_pipeline,
- project: forked_project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch)
+ create(
+ :ci_pipeline,
+ project: forked_project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch
+ )
end
before do
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index faa46ff4df1..5ce919fe2e6 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -15,11 +15,13 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
context 'with pipelines' do
let!(:pipeline) do
- create(:ci_pipeline,
- :success,
- project: merge_request.source_project,
- ref: merge_request.source_branch,
- sha: merge_request.diff_head_sha)
+ create(
+ :ci_pipeline,
+ :success,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
end
let!(:manual_job) { create(:ci_build, :manual, name: 'job1', stage: 'deploy', pipeline: pipeline) }
@@ -116,9 +118,14 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
let_it_be(:reporter_in_parent_and_developer_in_fork) { create(:user) }
let(:merge_request) do
- create(:merge_request, :with_detached_merge_request_pipeline,
- source_project: forked_project, source_branch: 'feature',
- target_project: parent_project, target_branch: 'master')
+ create(
+ :merge_request,
+ :with_detached_merge_request_pipeline,
+ source_project: forked_project,
+ source_branch: 'feature',
+ target_project: parent_project,
+ target_branch: 'master'
+ )
end
let(:config) do
@@ -193,7 +200,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
def create_merge_request_pipeline
page.within('.merge-request-tabs') { click_link('Pipelines') }
- click_button('Run pipeline')
+ click_on('Run pipeline')
end
def check_pipeline(expected_project:)
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 91f8fd13681..715cc2f73be 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -57,9 +57,9 @@ RSpec.describe 'Merge request > User sees versions', :js, feature_category: :cod
end
it_behaves_like 'allows commenting',
- file_name: '.gitmodules',
- line_text: '[submodule "six"]',
- comment: 'Typo, please fix.'
+ file_name: '.gitmodules',
+ line_text: '[submodule "six"]',
+ comment: 'Typo, please fix.'
end
describe 'switch between versions' do
@@ -105,9 +105,9 @@ RSpec.describe 'Merge request > User sees versions', :js, feature_category: :cod
end
it_behaves_like 'allows commenting',
- file_name: '.gitmodules',
- line_text: 'path = six',
- comment: 'Typo, please fix.'
+ file_name: '.gitmodules',
+ line_text: 'path = six',
+ comment: 'Typo, please fix.'
end
describe 'compare with older version' do
@@ -172,9 +172,9 @@ RSpec.describe 'Merge request > User sees versions', :js, feature_category: :cod
end
it_behaves_like 'allows commenting',
- file_name: '.gitmodules',
- line_text: '[submodule "gitlab-shell"]',
- comment: 'Typo, please fix.'
+ file_name: '.gitmodules',
+ line_text: '[submodule "gitlab-shell"]',
+ comment: 'Typo, please fix.'
end
describe 'compare with same version' do
@@ -239,8 +239,8 @@ RSpec.describe 'Merge request > User sees versions', :js, feature_category: :cod
end
it_behaves_like 'allows commenting',
- file_name: 'files/ruby/popen.rb',
- line_text: 'RuntimeError',
- comment: 'Typo, please fix.'
+ file_name: 'files/ruby/popen.rb',
+ line_text: 'RuntimeError',
+ comment: 'Typo, please fix.'
end
end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index dae28cbb05c..e3be99254dc 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -144,9 +144,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
context 'when a new merge request has a pipeline' do
let!(:pipeline) do
- create(:ci_pipeline, sha: project.commit('fix').id,
- ref: 'fix',
- project: project)
+ create(:ci_pipeline, sha: project.commit('fix').id, ref: 'fix', project: project)
end
it 'shows pipelines for a new merge request' do
diff --git a/spec/features/merge_request/user_squashes_merge_request_spec.rb b/spec/features/merge_request/user_squashes_merge_request_spec.rb
index 63faf830f7e..200f310d929 100644
--- a/spec/features/merge_request/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_request/user_squashes_merge_request_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User squashes a merge request', :js, feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:source_branch) { 'csv' }
@@ -12,19 +14,25 @@ RSpec.describe 'User squashes a merge request', :js, feature_category: :code_rev
shared_examples 'squash' do
it 'squashes the commits into a single commit, and adds a merge commit', :sidekiq_might_not_need_inline do
+ close_rich_text_promo_popover_if_present
+
expect(page).to have_content('Merged')
latest_master_commits = project.repository.commits_between(original_head.sha, 'master').map(&:raw)
- squash_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/),
- message: a_string_starting_with(project.merge_requests.first.default_squash_commit_message),
- author_name: user.name,
- committer_name: user.name)
+ squash_commit = an_object_having_attributes(
+ sha: a_string_matching(/\h{40}/),
+ message: a_string_starting_with(project.merge_requests.first.default_squash_commit_message),
+ author_name: user.name,
+ committer_name: user.name
+ )
- merge_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/),
- message: a_string_starting_with("Merge branch '#{source_branch}' into 'master'"),
- author_name: user.name,
- committer_name: user.name)
+ merge_commit = an_object_having_attributes(
+ sha: a_string_matching(/\h{40}/),
+ message: a_string_starting_with("Merge branch '#{source_branch}' into 'master'"),
+ author_name: user.name,
+ committer_name: user.name
+ )
expect(project.repository).not_to be_merged_to_root_ref(source_branch)
expect(latest_master_commits).to match([squash_commit, merge_commit])
@@ -33,12 +41,16 @@ RSpec.describe 'User squashes a merge request', :js, feature_category: :code_rev
shared_examples 'no squash' do
it 'accepts the merge request without squashing', :sidekiq_might_not_need_inline do
+ close_rich_text_promo_popover_if_present
+
expect(page).to have_content('Merged')
expect(project.repository).to be_merged_to_root_ref(source_branch)
end
end
def accept_mr
+ close_rich_text_promo_popover_if_present
+
expect(page).to have_button('Merge')
uncheck 'Delete source branch' unless protected_source_branch
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index efd88df0f97..1a814aeb89d 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_workflow do
include MergeRequestDiffHelpers
include RepoHelpers
+ include ContentEditorHelpers
def expect_suggestion_has_content(element, expected_changing_content, expected_suggested_content)
changing_content = element.all(:css, '.line_holder.old').map { |el| el.text(normalize_ws: true) }
@@ -35,6 +36,7 @@ RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_wo
context 'single suggestion note' do
it 'hides suggestion popover' do
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[1][:line_code]}']"))
+ close_rich_text_promo_popover_if_present
expect(page).to have_selector('.diff-suggest-popover')
@@ -303,13 +305,17 @@ RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_wo
"5 # heh"
]
- expect_suggestion_has_content(suggestion_1,
- suggestion_1_expected_changing_content,
- suggestion_1_expected_suggested_content)
-
- expect_suggestion_has_content(suggestion_2,
- suggestion_2_expected_changing_content,
- suggestion_2_expected_suggested_content)
+ expect_suggestion_has_content(
+ suggestion_1,
+ suggestion_1_expected_changing_content,
+ suggestion_1_expected_suggested_content
+ )
+
+ expect_suggestion_has_content(
+ suggestion_2,
+ suggestion_2_expected_changing_content,
+ suggestion_2_expected_suggested_content
+ )
end
end
end
diff --git a/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb b/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb
index 5770f5ab94d..1232e19d22b 100644
--- a/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb
+++ b/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb
@@ -3,19 +3,27 @@
require 'spec_helper'
RSpec.describe 'Merge Request > User tries to access private project information through the new mr page',
-feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
let(:current_user) { create(:user) }
let(:private_project) do
- create(:project, :public, :repository,
- path: 'nothing-to-see-here',
- name: 'nothing to see here',
- repository_access_level: ProjectFeature::PRIVATE)
+ create(
+ :project,
+ :public,
+ :repository,
+ path: 'nothing-to-see-here',
+ name: 'nothing to see here',
+ repository_access_level: ProjectFeature::PRIVATE
+ )
end
let(:owned_project) do
- create(:project, :public, :repository,
- namespace: current_user.namespace,
- creator: current_user)
+ create(
+ :project,
+ :public,
+ :repository,
+ namespace: current_user.namespace,
+ creator: current_user
+ )
end
context 'when the user enters the querystring info for the other project' do
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index 1ec86948065..1c63f5b56b0 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -8,7 +8,7 @@ require 'spec_helper'
# Because this kind of spec takes more time to run there is no need to add new ones
# for each existing quick action unless they test something not tested by existing tests.
RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_redis_caching,
-feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
include Features::NotesHelpers
let(:project) { create(:project, :public, :repository) }
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 1a9d40ae926..cd0ea639d4d 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
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User views an open merge request', feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project, description: '# Description header')
end
@@ -53,6 +55,7 @@ RSpec.describe 'User views an open merge request', feature_category: :code_revie
sign_in(user)
visit(edit_project_merge_request_path(project, merge_request))
+ close_rich_text_promo_popover_if_present
end
it 'renders empty description preview' do
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 371c40b40a5..f594e39b2b7 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -14,44 +14,52 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
let(:user5) { create(:user) }
before do
- @fix = create(:merge_request,
- title: 'fix',
- source_project: project,
- source_branch: 'fix',
- assignees: [user],
- reviewers: [user, user2, user3, user4, user5],
- milestone: create(:milestone, project: project, due_date: '2013-12-11'),
- created_at: 1.minute.ago,
- updated_at: 1.minute.ago)
+ @fix = create(
+ :merge_request,
+ title: 'fix',
+ source_project: project,
+ source_branch: 'fix',
+ assignees: [user],
+ reviewers: [user, user2, user3, user4, user5],
+ 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: 20.seconds.ago)
- @markdown = create(:merge_request,
- title: 'markdown',
- source_project: project,
- source_branch: 'markdown',
- assignees: [user],
- 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,
- state: 'merged')
+ @markdown = create(
+ :merge_request,
+ title: 'markdown',
+ source_project: project,
+ source_branch: 'markdown',
+ assignees: [user],
+ 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,
+ state: 'merged'
+ )
@markdown.metrics.update!(merged_at: 10.minutes.ago, latest_closed_at: 10.seconds.ago)
- @merge_test = create(:merge_request,
- title: 'merge-test',
- source_project: project,
- source_branch: 'merge-test',
- created_at: 3.minutes.ago,
- updated_at: 10.seconds.ago)
+ @merge_test = create(
+ :merge_request,
+ title: 'merge-test',
+ source_project: project,
+ source_branch: 'merge-test',
+ created_at: 3.minutes.ago,
+ updated_at: 10.seconds.ago
+ )
@merge_test.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.seconds.ago)
- @feature = create(:merge_request,
- title: 'feature',
- source_project: project,
- source_branch: 'feautre',
- created_at: 2.minutes.ago,
- updated_at: 1.minute.ago,
- state: 'merged')
+ @feature = create(
+ :merge_request,
+ title: 'feature',
+ source_project: project,
+ source_branch: 'feautre',
+ created_at: 2.minutes.ago,
+ updated_at: 1.minute.ago,
+ state: 'merged'
+ )
@feature.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.minutes.ago)
end
@@ -134,8 +142,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
label = create(:label, project: project)
create(:label_link, label: label, target: @fix)
- visit_merge_requests(project, label_name: [label.name],
- sort: sort_value_milestone)
+ visit_merge_requests(project, label_name: [label.name], sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
@@ -160,8 +167,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
end
it 'sorts by milestone due date' do
- visit_merge_requests(project, label_name: [label.name, label2.name],
- sort: sort_value_milestone)
+ visit_merge_requests(project, label_name: [label.name, label2.name], sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
@@ -169,9 +175,12 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
context 'filter on assignee and' do
it 'sorts by milestone due date' do
- visit_merge_requests(project, label_name: [label.name, label2.name],
- assignee_id: user.id,
- sort: sort_value_milestone)
+ visit_merge_requests(
+ project,
+ label_name: [label.name, label2.name],
+ assignee_id: user.id,
+ sort: sort_value_milestone
+ )
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
diff --git a/spec/features/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb
index 1a2024a5511..0021f701290 100644
--- a/spec/features/merge_requests/user_views_open_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb
@@ -57,10 +57,12 @@ RSpec.describe 'User views open merge requests', feature_category: :code_review_
let!(:build) { create :ci_build, pipeline: pipeline }
let(:merge_request) do
- create(:merge_request_with_diffs,
- source_project: project,
- target_project: project,
- source_branch: 'merge-test')
+ create(
+ :merge_request_with_diffs,
+ source_project: project,
+ target_project: project,
+ source_branch: 'merge-test'
+ )
end
let(:pipeline) do
diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb
index 74022a4a976..ccbf4646273 100644
--- a/spec/features/nav/top_nav_spec.rb
+++ b/spec/features/nav/top_nav_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
end
def invite_members_from_menu
- find('[data-testid="new-dropdown"]').click
+ find('[data-testid="new-menu-toggle"]').click
click_link('Invite members')
end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 084bf609a0d..d8501116134 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Member autocomplete', :js, feature_category: :groups_and_projects do
+ include Features::AutocompleteHelpers
+
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
@@ -85,10 +87,4 @@ RSpec.describe 'Member autocomplete', :js, feature_category: :groups_and_project
include_examples "open suggestions when typing @", 'commit'
end
-
- private
-
- def find_autocomplete_menu
- find('.atwho-view ul', visible: true)
- end
end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 578025e1494..14fc6ed33b3 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -49,43 +49,127 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
expect(page).not_to have_selector('.file-content')
end
- context 'when user has groups' do
- let(:group) do
- create :group do |group|
- group.add_owner(user)
+ context 'for tabs' do
+ shared_examples_for 'shows expected content' do
+ it 'shows expected content', :js do
+ visit(user_path(user))
+
+ page.within ".cover-block" do
+ expect(page).to have_content user.name
+ expect(page).to have_content user.username
+ end
+
+ page.within ".content" do
+ click_link link
+ end
+
+ page.within div do
+ expect(page).to have_content expected_content
+ end
end
end
- let!(:project) do
- create(:project, :repository, namespace: group) do |project|
- create(:closed_issue_event, project: project)
- project.add_maintainer(user)
+ context 'for Groups' do
+ let_it_be(:group) do
+ create :group do |group|
+ group.add_owner(user)
+ end
+ end
+
+ let_it_be(:project) do
+ create(:project, :repository, namespace: group) do |project|
+ create(:closed_issue_event, project: project)
+ project.add_maintainer(user)
+ end
+ end
+
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Groups' }
+ let(:div) { '#groups' }
+ let(:expected_content) { group.name }
+ end
+ end
+
+ context 'for Contributed projects' do
+ let_it_be(:project) do
+ create(:project) do |project|
+ project.add_maintainer(user)
+ end
+ end
+
+ before do
+ push_event = create(:push_event, project: project, author: user)
+ create(:push_event_payload, event: push_event)
+ end
+
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Contributed projects' }
+ let(:div) { '#contributed' }
+ let(:expected_content) { project.name }
+ end
+ end
+
+ context 'for personal projects' do
+ let_it_be(:project) do
+ create(:project, namespace: user.namespace)
+ end
+
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Personal projects' }
+ let(:div) { '#projects' }
+ let(:expected_content) { project.name }
end
end
- def click_on_profile_picture
- find(:css, '.header-user-dropdown-toggle').click
+ context 'for starred projects' do
+ let_it_be(:project) { create(:project, :public) }
- page.within ".header-user" do
- click_link user.username
+ before do
+ user.toggle_star(project)
+ end
+
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Starred projects' }
+ let(:div) { '#starred' }
+ let(:expected_content) { project.name }
end
end
- it 'shows user groups', :js do
- visit(profile_path)
- click_on_profile_picture
+ context 'for snippets' do
+ let_it_be(:snippet) { create(:snippet, :public, author: user) }
- page.within ".cover-block" do
- expect(page).to have_content user.name
- expect(page).to have_content user.username
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Snippets' }
+ let(:div) { '#snippets' }
+ let(:expected_content) { snippet.title }
end
+ end
+
+ context 'for followers' do
+ let_it_be(:fan) { create(:user) }
+
+ before do
+ fan.follow(user)
+ end
+
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Followers' }
+ let(:div) { '#followers' }
+ let(:expected_content) { fan.name }
+ end
+ end
+
+ context 'for following' do
+ let_it_be(:star) { create(:user) }
- page.within ".content" do
- click_link "Groups"
+ before do
+ user.follow(star)
end
- page.within "#groups" do
- expect(page).to have_content group.name
+ it_behaves_like 'shows expected content' do
+ let(:link) { 'Following' }
+ let(:div) { '#following' }
+ let(:expected_content) { star.name }
end
end
end
diff --git a/spec/features/projects/activity/user_sees_activity_spec.rb b/spec/features/projects/activity/user_sees_activity_spec.rb
index 5335b9d0e95..7940ded41a9 100644
--- a/spec/features/projects/activity/user_sees_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_activity_spec.rb
@@ -10,12 +10,14 @@ RSpec.describe 'Projects > Activity > User sees activity', feature_category: :gr
before do
create(:event, :created, project: project, target: issue, author: user)
event = create(:push_event, project: project, author: user)
- create(:push_event_payload,
- event: event,
- action: :created,
- commit_to: '6d394385cf567f80a8fd85055db1ab4c5295806f',
- ref: 'fix',
- commit_count: 1)
+ create(
+ :push_event_payload,
+ event: event,
+ action: :created,
+ commit_to: '6d394385cf567f80a8fd85055db1ab4c5295806f',
+ ref: 'fix',
+ commit_count: 1
+ )
end
it 'shows the last push in the activity page', :js do
diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
index 48dcb95e09b..eaf57c566e8 100644
--- a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe "User downloads artifacts", feature_category: :build_artifacts do
shared_examples "downloading" do
it "downloads the zip" do
- expect(page.response_headers['Content-Disposition']).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
+ expect(page.response_headers['Content-Disposition']).to eq(%{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
expect(page.response_headers['Content-Transfer-Encoding']).to eq("binary")
expect(page.response_headers['Content-Type']).to eq("application/zip")
expect(page.source.b).to eq(job.artifacts_file.file.read.b)
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 3c8b17607ca..c4f20ff74b2 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -190,14 +190,21 @@ RSpec.describe 'test coverage badge', feature_category: :code_testing do
end
def show_test_coverage_badge(job: nil, min_good: nil, min_acceptable: nil, min_medium: nil)
- visit coverage_project_badges_path(project, ref: :master, job: job, min_good: min_good,
- min_acceptable: min_acceptable, min_medium: min_medium, format: :svg)
+ visit coverage_project_badges_path(
+ project,
+ ref: :master,
+ job: job,
+ min_good: min_good,
+ min_acceptable: min_acceptable,
+ min_medium: min_medium,
+ format: :svg
+ )
end
def expect_coverage_badge(coverage)
svg = Nokogiri::XML.parse(page.body)
expect(page.response_headers['Content-Type']).to include('image/svg+xml')
- expect(svg.at(%Q{text:contains("#{coverage}")})).to be_truthy
+ expect(svg.at(%{text:contains("#{coverage}")})).to be_truthy
end
def expect_coverage_badge_color(color)
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index c0f5d0ffead..94772c9fc1e 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe 'Pipeline Badge', feature_category: :continuous_integration do
def expect_badge(status)
svg = Nokogiri::XML.parse(page.body)
expect(page.response_headers['Content-Type']).to include('image/svg+xml')
- expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy
+ expect(svg.at(%{text:contains("#{status}")})).to be_truthy
end
end
end
diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb
index 798cd401dac..dfda200cded 100644
--- a/spec/features/projects/blobs/blame_spec.rb
+++ b/spec/features/projects/blobs/blame_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'File blame', :js, feature_category: :groups_and_projects do
+RSpec.describe 'File blame', :js, feature_category: :source_code_management do
include TreeHelper
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 62cd9fd9a56..30a81ccc071 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -943,7 +943,7 @@ RSpec.describe 'File blob', :js, feature_category: :groups_and_projects do
page.within('.commit-actions') do
expect(page).to have_css('.ci-status-icon')
expect(page).to have_css('.ci-status-icon-running')
- expect(page).to have_css('.js-ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running-icon"]')
end
end
end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 2092af537e8..a888b5b977d 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -9,18 +9,24 @@ RSpec.describe 'Download buttons in branches page', feature_category: :groups_an
let(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit('binary-encoding').sha,
- ref: 'binary-encoding', # make sure the branch is in the 1st page!
- status: status)
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit('binary-encoding').sha,
+ ref: 'binary-encoding', # make sure the branch is in the 1st page!
+ status: status
+ )
end
let!(:build) do
- create(:ci_build, :success, :artifacts,
- pipeline: pipeline,
- status: pipeline.status,
- name: 'build')
+ create(
+ :ci_build,
+ :success,
+ :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build'
+ )
end
before do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 6a13d5637af..50df7bb7ca5 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -95,15 +95,22 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
it 'shows only default_per_page active branches sorted by last updated' do
visit project_branches_filtered_path(project, state: 'active')
- expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page,
- sort_by: :updated_desc, state: 'active'))
+ expect(page).to have_content(sorted_branches(
+ repository,
+ count: Kaminari.config.default_per_page,
+ sort_by: :updated_desc,
+ state: 'active'
+ ))
end
it 'shows only default_per_page branches sorted by last updated on All branches' do
visit project_branches_filtered_path(project, state: 'all')
- expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page,
- sort_by: :updated_desc))
+ expect(page).to have_content(sorted_branches(
+ repository,
+ count: Kaminari.config.default_per_page,
+ sort_by: :updated_desc
+ ))
end
end
end
@@ -161,11 +168,15 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
end
it 'avoids a N+1 query in branches index' do
+ new_branches_count = 20
+ sql_queries_count_threshold = 10
+
control_count = ActiveRecord::QueryRecorder.new { visit project_branches_path(project) }.count
- %w[one two three four five].each { |ref| repository.add_branch(user, ref, 'master') }
+ (1..new_branches_count).each { |number| repository.add_branch(user, "new-branch-#{number}", 'master') }
- expect { visit project_branches_filtered_path(project, state: 'all') }.not_to exceed_query_limit(control_count)
+ expect { visit project_branches_filtered_path(project, state: 'all') }
+ .not_to exceed_query_limit(control_count).with_threshold(sql_queries_count_threshold)
end
end
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index 9851194bd3c..43da57c16d1 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -10,18 +10,144 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
let(:default_branch) { 'main' }
let(:other_branch) { 'test' }
+ let(:branch_with_invalid_ci) { 'despair' }
+
+ let(:default_content) { 'Default' }
+
+ let(:valid_content) do
+ <<~YAML
+ ---
+ stages:
+ - Build
+ - Test
+ job_a:
+ script: echo hello
+ stage: Build
+ job_b:
+ script: echo hello from job b
+ stage: Test
+ YAML
+ end
+
+ let(:invalid_content) do
+ <<~YAML
+
+ job3:
+ stage: stage_foo
+ script: echo 'Done.'
+ YAML
+ end
before do
sign_in(user)
project.add_developer(user)
- project.repository.create_file(user, project.ci_config_path_or_default, 'Default Content', message: 'Create CI file for main', branch_name: default_branch)
- project.repository.create_file(user, project.ci_config_path_or_default, 'Other Content', message: 'Create CI file for test', branch_name: other_branch)
+ project.repository.create_file(user, project.ci_config_path_or_default, default_content, message: 'Create CI file for main', branch_name: default_branch)
+ project.repository.create_file(user, project.ci_config_path_or_default, valid_content, message: 'Create CI file for test', branch_name: other_branch)
+ project.repository.create_file(user, project.ci_config_path_or_default, invalid_content, message: 'Create CI file for test', branch_name: branch_with_invalid_ci)
visit project_ci_pipeline_editor_path(project)
wait_for_requests
end
+ describe 'Default tabs' do
+ it 'renders the edit tab as the default' do
+ expect(page).to have_selector('[data-testid="editor-tab"]')
+ end
+
+ it 'renders the visualize, validate and full configuration tabs', :aggregate_failures do
+ expect(page).to have_selector('[data-testid="visualization-tab"]', visible: :hidden)
+ expect(page).to have_selector('[data-testid="validate-tab"]', visible: :hidden)
+ expect(page).to have_selector('[data-testid="merged-tab"]', visible: :hidden)
+ end
+ end
+
+ describe 'When CI yml has valid syntax' do
+ before do
+ visit project_ci_pipeline_editor_path(project, branch_name: other_branch)
+ wait_for_requests
+ end
+
+ it 'shows "Pipeline syntax is correct" in the lint widget' do
+ page.within('[data-testid="validation-segment"]') do
+ expect(page).to have_content("Pipeline syntax is correct")
+ end
+ end
+
+ it 'shows the graph in the visualization tab' do
+ click_link "Visualize"
+
+ page.within('[data-testid="graph-container"') do
+ expect(page).to have_content("job_a")
+ end
+ end
+
+ it 'can simulate pipeline in the validate tab' do
+ click_link "Validate"
+
+ click_button "Validate pipeline"
+ wait_for_requests
+
+ expect(page).to have_content("Simulation completed successfully")
+ end
+
+ it 'renders the merged yaml in the full configuration tab' do
+ click_link "Full configuration"
+
+ page.within('[data-testid="merged-tab"') do
+ expect(page).to have_content("job_a")
+ end
+ end
+ end
+
+ describe 'When CI yml has invalid syntax' do
+ before do
+ visit project_ci_pipeline_editor_path(project, branch_name: branch_with_invalid_ci)
+ wait_for_requests
+ end
+
+ it 'shows "Syntax is invalid" in the lint widget' do
+ page.within('[data-testid="validation-segment"]') do
+ expect(page).to have_content("This GitLab CI configuration is invalid")
+ end
+ end
+
+ it 'does not render the graph in the visualization tab and shows error' do
+ click_link "Visualize"
+
+ expect(page).not_to have_selector('[data-testid="graph-container"')
+ expect(page).to have_content("Your CI/CD configuration syntax is invalid. Select the Validate tab for more details.")
+ end
+
+ it 'gets a simulation error in the validate tab' do
+ click_link "Validate"
+
+ click_button "Validate pipeline"
+ wait_for_requests
+
+ expect(page).to have_content("Pipeline simulation completed with errors")
+ end
+
+ it 'renders merged yaml config' do
+ click_link "Full configuration"
+
+ page.within('[data-testid="merged-tab"') do
+ expect(page).to have_content("job3")
+ end
+ end
+ end
+
+ describe 'with unparsable yaml' do
+ it 'renders an error in the merged yaml tab' do
+ click_link "Full configuration"
+
+ page.within('[data-testid="merged-tab"') do
+ expect(page).not_to have_content("job_a")
+ expect(page).to have_content("Could not load full configuration content")
+ end
+ end
+ end
+
shared_examples 'default branch switcher behavior' do
def switch_to_branch(branch)
find('[data-testid="branch-selector"]').click
@@ -109,7 +235,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
expect(page).to have_content('Pipeline Editor')
page.within('#source-editor-') do
- expect(page).to have_content('Default Content123')
+ expect(page).to have_content("#{default_content}123")
end
end
@@ -166,8 +292,8 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end
page.within('#source-editor-') do
- expect(page).to have_content('Default Content')
- expect(page).not_to have_content('Default Content123')
+ expect(page).to have_content(default_content)
+ expect(page).not_to have_content("#{default_content}123")
end
end
@@ -188,7 +314,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end
page.within('#source-editor-') do
- expect(page).to have_content('Default Content123')
+ expect(page).to have_content("#{default_content}123")
end
end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index f9195904ea3..eadcc0e62c4 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'Gcp Cluster', :js, feature_category: :deployment_management do
before do
visit project_clusters_path(project)
- click_button(class: 'dropdown-toggle-split')
+ click_button(class: 'gl-new-dropdown-toggle')
click_link 'Connect a cluster (certificate - deprecated)'
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index eb2601bb85f..6da8eea687e 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'User Cluster', :js, feature_category: :deployment_management do
before do
visit project_clusters_path(project)
- click_button(class: 'dropdown-toggle-split')
+ click_button(class: 'gl-new-dropdown-toggle')
click_link 'Connect a cluster (certificate - deprecated)'
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index e2737d62749..d40f929d0b2 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -125,12 +125,12 @@ RSpec.describe 'Clusters', :js, feature_category: :groups_and_projects do
def visit_create_cluster_page
visit project_clusters_path(project)
- click_button(class: 'dropdown-toggle-split')
+ click_button(class: 'gl-new-dropdown-toggle')
click_link 'Create a cluster'
end
def visit_connect_cluster_page
- click_button(class: 'dropdown-toggle-split')
+ click_button(class: 'gl-new-dropdown-toggle')
click_link 'Connect a cluster (certificate - deprecated)'
end
end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index dfd58a99953..54a189692ce 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -6,9 +6,7 @@ RSpec.describe 'project commit pipelines', :js, feature_category: :continuous_in
let(:project) { create(:project, :repository) }
before do
- create(:ci_pipeline, project: project,
- sha: project.commit.sha,
- ref: 'master')
+ create(:ci_pipeline, project: project, sha: project.commit.sha, ref: 'master')
user = create(:user)
project.add_maintainer(user)
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 3611efd1477..d2104799e79 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -7,11 +7,13 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
context 'when commit has pipelines' do
let(:pipeline) do
- create(:ci_pipeline,
- status: :running,
- project: project,
- ref: project.default_branch,
- sha: project.commit.sha)
+ create(
+ :ci_pipeline,
+ status: :running,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha
+ )
end
let(:build) { create(:ci_build, pipeline: pipeline, status: :running) }
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 791f626b8d9..3513e249b63 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -175,9 +175,13 @@ RSpec.describe 'User browses commits', feature_category: :source_code_management
let(:confidential_issue) { create(:issue, confidential: true, title: 'Secret issue!', project: project) }
before do
- project.repository.create_file(user, 'dummy-file', 'dummy content',
- branch_name: 'feature',
- message: "Linking #{confidential_issue.to_reference}")
+ project.repository.create_file(
+ user,
+ 'dummy-file',
+ 'dummy content',
+ branch_name: 'feature',
+ message: "Linking #{confidential_issue.to_reference}"
+ )
end
context 'when the user cannot see confidential issues but was cached with a link', :use_clean_rails_memory_store_fragment_caching do
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index beb5fa7822b..eff538513c1 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
click_button 'Compare'
- expect(page).to have_content 'Commits'
+ expect(page).to have_content 'Commits on Source'
expect(page).to have_link 'Create merge request'
end
end
@@ -53,7 +53,7 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
select_using_dropdown('to', RepoHelpers.sample_commit.id, commit: true)
click_button 'Compare'
- expect(page).to have_content 'Commits (1)'
+ expect(page).to have_content 'Commits on Source (1)'
expect(page).to have_content "Showing 2 changed files"
diff = first('.js-unfold')
@@ -85,7 +85,7 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
click_button 'Compare'
- expect(page).to have_content 'Commits (1)'
+ expect(page).to have_content 'Commits on Source (1)'
expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions'
expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request)
expect(page).not_to have_link 'Create merge request'
@@ -136,14 +136,14 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
visit project_compare_index_path(project, from: "feature", to: "master")
click_button('Compare')
- expect(page).to have_content 'Commits (29)'
+ expect(page).to have_content 'Commits on Source (29)'
# go to the second page
within(".files .gl-pagination") do
click_on("2")
end
- expect(page).not_to have_content 'Commits (29)'
+ expect(page).not_to have_content 'Commits on Source (29)'
end
end
end
@@ -159,7 +159,7 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
expect(find(".js-compare-to-dropdown .gl-dropdown-button-text")).to have_content("v1.1.0")
click_button "Compare"
- expect(page).to have_content "Commits"
+ expect(page).to have_content "Commits on Source"
end
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 0f903901984..11ea72b87a2 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -300,14 +300,12 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
context 'with manual action' do
let(:action) do
- create(:ci_build, :manual, pipeline: pipeline,
- name: 'deploy to production', environment: environment.name)
+ create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production', environment: environment.name)
end
context 'when user has ability to trigger deployment' do
let(:permissions) do
- create(:protected_branch, :developers_can_merge,
- name: action.ref, project: project)
+ create(:protected_branch, :developers_can_merge, name: action.ref, project: project)
end
it 'does show a play button' do
@@ -331,8 +329,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
context 'when user has no ability to trigger a deployment' do
let(:permissions) do
- create(:protected_branch, :no_one_can_merge,
- name: action.ref, project: project)
+ create(:protected_branch, :no_one_can_merge, name: action.ref, project: project)
end
it 'does not show a play button' do
@@ -391,10 +388,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
end
let(:deployment) do
- create(:deployment, :success,
- environment: environment,
- deployable: build,
- on_stop: 'close_app')
+ create(:deployment, :success, environment: environment, deployable: build, on_stop: 'close_app')
end
context 'when user has ability to stop environment' do
@@ -411,8 +405,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
context 'when user has no ability to stop environment' do
let(:permissions) do
- create(:protected_branch, :no_one_can_merge,
- name: action.ref, project: project)
+ create(:protected_branch, :no_one_can_merge, name: action.ref, project: project)
end
it 'does not allow to stop environment', :js do
@@ -445,9 +438,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
describe 'environment folders', :js do
context 'when folder name contains special charaters' do
before do
- create(:environment, project: project,
- name: 'staging-1.0/review',
- state: :available)
+ create(:environment, project: project, name: 'staging-1.0/review', state: :available)
end
it 'renders a correct environment folder' do
@@ -465,8 +456,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
let(:project) { create(:project, :repository) }
let!(:environment) do
- create(:environment, :with_review_app, project: project,
- ref: 'feature')
+ create(:environment, :with_review_app, project: project, ref: 'feature')
end
it 'user visits environment page', :js do
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 2490b1fde8e..3a2c7f0ac7b 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects do
+RSpec.describe 'Environments page', :js, feature_category: :continuous_delivery do
include Spec::Support::Helpers::ModalHelpers
let(:project) { create(:project) }
@@ -159,9 +159,7 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
let(:project) { create(:project, :repository) }
let!(:deployment) do
- create(:deployment, :success,
- environment: environment,
- sha: project.commit.id)
+ create(:deployment, :success, environment: environment, sha: project.commit.id)
end
it 'shows deployment SHA and internal ID' do
@@ -182,10 +180,7 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
end
let!(:deployment) do
- create(:deployment, :success,
- environment: environment,
- deployable: build,
- sha: project.commit.id)
+ create(:deployment, :success, environment: environment, deployable: build, sha: project.commit.id)
end
before do
@@ -241,10 +236,7 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
end
let(:deployment) do
- create(:deployment, :success,
- environment: environment,
- deployable: build,
- on_stop: 'close_app')
+ create(:deployment, :success, environment: environment, deployable: build, on_stop: 'close_app')
end
it 'shows a stop button and dialog' do
@@ -296,18 +288,11 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
let!(:build) { create(:ci_build, pipeline: pipeline) }
let!(:delayed_job) do
- create(:ci_build, :scheduled,
- pipeline: pipeline,
- name: 'delayed job',
- stage: 'test')
+ create(:ci_build, :scheduled, pipeline: pipeline, name: 'delayed job', stage: 'test')
end
let!(:deployment) do
- create(:deployment,
- :success,
- environment: environment,
- deployable: build,
- sha: project.commit.id)
+ create(:deployment, :success, environment: environment, deployable: build, sha: project.commit.id)
end
before do
@@ -327,10 +312,7 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
context 'when delayed job is expired already' do
let!(:delayed_job) do
- create(:ci_build, :expired_scheduled,
- pipeline: pipeline,
- name: 'delayed job',
- stage: 'test')
+ create(:ci_build, :expired_scheduled, pipeline: pipeline, name: 'delayed job', stage: 'test')
end
it "shows 00:00:00 as the remaining time" do
@@ -365,9 +347,7 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
let(:project) { create(:project, :repository) }
let!(:deployment) do
- create(:deployment, :failed,
- environment: environment,
- sha: project.commit.id)
+ create(:deployment, :failed, environment: environment, sha: project.commit.id)
end
it 'does not show deployments', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409990' do
@@ -382,9 +362,7 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
let_it_be(:project) { create(:project, :repository) }
let!(:deployment) do
- create(:deployment, :running,
- environment: environment,
- sha: project.commit.id)
+ create(:deployment, :running, environment: environment, sha: project.commit.id)
end
it "renders the upcoming deployment", :aggregate_failures do
@@ -443,14 +421,8 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
describe 'environments folders' do
describe 'available environments' do
before do
- create(:environment, :will_auto_stop,
- project: project,
- name: 'staging/review-1',
- state: :available)
- create(:environment, :will_auto_stop,
- project: project,
- name: 'staging/review-2',
- state: :available)
+ create(:environment, :will_auto_stop, project: project, name: 'staging/review-1', state: :available)
+ create(:environment, :will_auto_stop, project: project, name: 'staging/review-2', state: :available)
end
it 'users unfurls an environment folder' do
@@ -470,14 +442,8 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
describe 'stopped environments' do
before do
- create(:environment, :will_auto_stop,
- project: project,
- name: 'staging/review-1',
- state: :stopped)
- create(:environment, :will_auto_stop,
- project: project,
- name: 'staging/review-2',
- state: :stopped)
+ create(:environment, :will_auto_stop, project: project, name: 'staging/review-1', state: :stopped)
+ create(:environment, :will_auto_stop, project: project, name: 'staging/review-2', state: :stopped)
end
it 'users unfurls an environment folder' do
@@ -497,12 +463,8 @@ RSpec.describe 'Environments page', :js, feature_category: :groups_and_projects
describe 'environments folders view' do
before do
- create(:environment, project: project,
- name: 'staging.review/review-1',
- state: :available)
- create(:environment, project: project,
- name: 'staging.review/review-2',
- state: :available)
+ create(:environment, project: project, name: 'staging.review/review-1', state: :available)
+ create(:environment, project: project, name: 'staging.review/review-2', state: :available)
end
it 'user opens folder view' do
diff --git a/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb
index 852d7bca96a..2c8d7275fbf 100644
--- a/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb
+++ b/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb
@@ -9,8 +9,7 @@ RSpec.describe 'User deletes feature flag', :js, feature_category: :feature_flag
let(:project) { create(:project, namespace: user.namespace) }
let!(:feature_flag) do
- create_flag(project, 'ci_live_trace', false,
- description: 'For live trace feature')
+ create_flag(project, 'ci_live_trace', false, description: 'For live trace feature')
end
before do
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index 9b3d19cfea3..81bd0523c70 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -7,18 +7,11 @@ RSpec.describe 'Projects > Files > Download buttons in files tree', feature_cate
let(:user) { project.creator }
let(:pipeline) 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!(:build) do
- create(:ci_build, :success, :artifacts,
- pipeline: pipeline,
- status: pipeline.status,
- name: 'build')
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, status: pipeline.status, name: 'build')
end
before do
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index b4edd5c2729..6efe1eb1ad1 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -13,16 +13,14 @@ RSpec.describe 'Projects > Files > User wants to edit a file', feature_category:
commit_message: "Committing First Update",
file_path: ".gitignore",
file_content: "First Update",
- last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch,
- ".gitignore").sha
+ last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, ".gitignore").sha
}
end
context 'when the user has write access' do
before do
sign_in user
- visit project_edit_blob_path(project,
- File.join(project.default_branch, '.gitignore'))
+ visit project_edit_blob_path(project, File.join(project.default_branch, '.gitignore'))
end
it 'file has been updated since the user opened the edit page' do
@@ -43,8 +41,7 @@ RSpec.describe 'Projects > Files > User wants to edit a file', feature_category:
before do
forked_project
sign_in user
- visit project_edit_blob_path(project,
- File.join(project.default_branch, '.gitignore'))
+ visit project_edit_blob_path(project, File.join(project.default_branch, '.gitignore'))
end
context 'and the forked project is ahead of the upstream project' do
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index bfe1fd073c5..c8543764d15 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Project owner sees a link to create a license file in empty project', :js,
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
include Features::WebIdeSpecHelpers
let(:project) { create(:project_empty_repo) }
diff --git a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
index 645bfeb14e3..37b718061c6 100644
--- a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
+++ b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# This is a regression test for https://gitlab.com/gitlab-org/gitlab-foss/issues/37569
RSpec.describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js,
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let(:project) { create(:project, :empty_repo) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index bb14b9c4e31..3b30a620257 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -134,7 +134,7 @@ RSpec.describe "User browses files", :js, feature_category: :groups_and_projects
click_link("Rake tasks")
expect(page).to have_current_path(project_tree_path(project, "markdown/doc/raketasks"), ignore_query: true)
- expect(page).to have_content("backup_restore.md").and have_content("maintenance.md")
+ expect(page).to have_content("maintenance.md")
click_link("maintenance.md")
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 070b6dbec7d..d824b3b1759 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ
context 'with default target branch' do
before do
first('.add-to-tree').click
- click_link('New directory')
+ click_button('New directory')
end
it 'creates the directory in the default branch' do
@@ -55,7 +55,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ
end
first('.add-to-tree').click
- click_link('New directory')
+ click_button('New directory')
fill_in(:dir_name, with: 'new_directory')
click_button('Create directory')
@@ -68,7 +68,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ
context 'with a new target branch' do
before do
first('.add-to-tree').click
- click_link('New directory')
+ click_button('New directory')
fill_in(:dir_name, with: 'new_directory')
fill_in(:branch_name, with: 'new-feature')
click_button('Create directory')
@@ -99,7 +99,7 @@ RSpec.describe 'Projects > Files > User creates a directory', :js, feature_categ
find('.add-to-tree').click
wait_for_requests
- click_link('New directory')
+ click_button('New directory')
fill_in(:dir_name, with: 'new_directory')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Create directory')
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 3c39d8745a4..ad2fccc14bf 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# Integration test that exports a file using the Import/Export feature
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
# we'll have to either include it adding the model that includes it to the +safe_list+
-# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
+# or make sure the attribute is denylisted in the +import_export.yml+ configuration
RSpec.describe 'Import/Export - project export integration test', :js, feature_category: :importers do
include ExportFileHelper
diff --git a/spec/features/projects/integrations/user_activates_prometheus_spec.rb b/spec/features/projects/integrations/user_activates_prometheus_spec.rb
index a47000672ca..db71256b294 100644
--- a/spec/features/projects/integrations/user_activates_prometheus_spec.rb
+++ b/spec/features/projects/integrations/user_activates_prometheus_spec.rb
@@ -13,7 +13,6 @@ RSpec.describe 'User activates Prometheus', feature_category: :integrations do
it 'saves and activates integration', :js do
visit_project_integration('Prometheus')
check('Active')
- fill_in('API URL', with: 'http://prometheus.example.com')
click_button('Save changes')
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 72695680809..4221fa26e00 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects do
include ProjectForksHelper
include CookieHelper
+ include ContentEditorHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -18,7 +19,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' }
- let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
+ let(:longtemplate_content) { %(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
let(:description_addition) { ' appending to description' }
@@ -36,6 +37,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
+ close_rich_text_promo_popover_if_present
page.find('.js-issuable-edit').click
fill_in :'issuable-title', with: 'test issue title'
end
@@ -79,6 +81,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
+ close_rich_text_promo_popover_if_present
page.find('.js-issuable-edit').click
fill_in :'issuable-title', with: 'test issue title'
fill_in :'issue-description', with: prior_description
@@ -108,6 +111,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
it 'does not overwrite autosaved description' do
visit new_project_issue_path project
wait_for_requests
+ close_rich_text_promo_popover_if_present
assert_template # default template is loaded the first time
@@ -141,6 +145,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
message: 'added merge request bug template',
branch_name: 'master')
visit edit_project_merge_request_path project, merge_request
+ close_rich_text_promo_popover_if_present
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -200,6 +205,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :groups_and_projects
message: 'added merge request template',
branch_name: 'master')
visit edit_project_merge_request_path project, merge_request
+ close_rich_text_promo_popover_if_present
fill_in :'merge_request[title]', with: 'test merge request title'
end
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
index 268c209cba1..bd9d1092e17 100644
--- a/spec/features/projects/issues/design_management/user_views_design_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb
@@ -5,16 +5,67 @@ require 'spec_helper'
RSpec.describe 'User views issue designs', :js, feature_category: :design_management do
include DesignManagementTestHelpers
+ let_it_be(:user) { create(:user) }
+ let_it_be(:guest_user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:design) { create(:design, :with_file, issue: issue) }
+ let_it_be(:note) { create(:diff_note_on_design, noteable: design, author: user) }
+
+ def add_diff_note_emoji(diff_note, emoji_name)
+ page.within(first(".image-notes li#note_#{diff_note.id}.design-note")) do
+ page.find('[data-testid="note-emoji-button"] .note-emoji-button').click
+
+ page.within('ul.dropdown-menu') do
+ page.find('input[type="search"]').set(emoji_name)
+ page.find('button[data-testid="emoji-button"]:first-child').click
+ end
+ end
+ end
+
+ def remove_diff_note_emoji(diff_note, emoji_name)
+ page.within(first(".image-notes li#note_#{diff_note.id}.design-note")) do
+ page.find(".awards button[data-emoji-name='#{emoji_name}']").click
+ end
+ end
+
+ before_all do
+ project.add_maintainer(user)
+ project.add_guest(guest_user)
+ end
before do
enable_design_management
+ sign_in(user)
+
visit project_issue_path(project, issue)
end
+ shared_examples 'design discussion emoji awards' do
+ it 'allows user to add emoji reaction to a comment' do
+ click_link design.filename
+
+ add_diff_note_emoji(note, 'thumbsup')
+
+ expect(page.find("li#note_#{note.id} .awards")).to have_selector('button[title="You reacted with :thumbsup:"]')
+ end
+
+ it 'allows user to remove emoji reaction from a comment' do
+ click_link design.filename
+
+ add_diff_note_emoji(note, 'thumbsup')
+
+ # Wait for emoji to be added
+ wait_for_requests
+
+ remove_diff_note_emoji(note, 'thumbsup')
+
+ # Only award emoji that was present has been removed
+ expect(page.find("li#note_#{note.id}")).not_to have_selector('.awards')
+ end
+ end
+
it 'opens design detail' do
click_link design.filename
@@ -25,6 +76,26 @@ RSpec.describe 'User views issue designs', :js, feature_category: :design_manage
expect(page).to have_selector('.js-design-image')
end
+ it 'shows a comment within design' do
+ click_link design.filename
+
+ expect(page.find('.image-notes .design-note .note-text')).to have_content(note.note)
+ end
+
+ it_behaves_like 'design discussion emoji awards'
+
+ context 'when user is guest' do
+ before do
+ enable_design_management
+
+ sign_in(guest_user)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it_behaves_like 'design discussion emoji awards'
+ end
+
context 'when svg file is loaded in design detail' do
let_it_be(:file) { Rails.root.join('spec/fixtures/svg_without_attr.svg') }
let_it_be(:design) { create(:design, :with_file, filename: 'svg_without_attr.svg', file: file, issue: issue) }
diff --git a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
index 3d40bae8544..bf1b2f7e5cd 100644
--- a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
+++ b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
@@ -8,26 +8,19 @@ RSpec.describe 'viewing an issue with cross project references' do
let(:user) { create(:user) }
let(:other_project) do
- create(:project, :public,
- external_authorization_classification_label: 'other_label')
+ create(:project, :public, external_authorization_classification_label: 'other_label')
end
let(:other_issue) do
- create(:issue, :closed,
- title: 'I am in another project',
- project: other_project)
+ create(:issue, :closed, title: 'I am in another project', project: other_project)
end
let(:other_confidential_issue) do
- create(:issue, :confidential, :closed,
- title: 'I am in another project and confidential',
- project: other_project)
+ create(:issue, :confidential, :closed, title: 'I am in another project and confidential', project: other_project)
end
let(:other_merge_request) do
- create(:merge_request, :closed,
- title: 'I am a merge request in another project',
- source_project: other_project)
+ create(:merge_request, :closed, title: 'I am a merge request in another project', source_project: other_project)
end
let(:description_referencing_other_issue) do
@@ -39,15 +32,11 @@ RSpec.describe 'viewing an issue with cross project references' do
let(:project) { create(:project) }
let(:issue) do
- create(:issue,
- project: project,
- description: description_referencing_other_issue)
+ create(:issue, project: project, description: description_referencing_other_issue)
end
let(:confidential_issue) do
- create(:issue, :confidential, :closed,
- title: "I am in the same project and confidential",
- project: project)
+ create(:issue, :confidential, :closed, title: "I am in the same project and confidential", project: project)
end
before do
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index aeba53c22b6..77f95827d88 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -79,8 +79,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
context 'when a job can be retried' do
let!(:job) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ create(:ci_build, pipeline: pipeline, stage: 'test')
end
before do
@@ -148,10 +147,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
context 'with downloadable artifacts' do
let!(:with_artifacts) do
- build = create(:ci_build, :success,
- pipeline: pipeline,
- name: 'rspec tests',
- stage: 'test')
+ build = create(:ci_build, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test')
create(:ci_job_artifact, :archive, job: build)
end
@@ -167,10 +163,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
context 'with artifacts expired' do
let!(:with_artifacts_expired) do
- create(:ci_build, :expired, :success,
- pipeline: pipeline,
- name: 'rspec',
- stage: 'test')
+ create(:ci_build, :expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test')
end
before do
@@ -188,8 +181,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
context 'column links' do
let!(:job) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ create(:ci_build, pipeline: pipeline, stage: 'test')
end
before do
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index fcd07d33535..c203e644280 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -306,7 +306,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
artifact_request = requests.find { |req| req.url.include?('artifacts/download') }
- expect(artifact_request.response_headers['Content-Disposition']).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
+ expect(artifact_request.response_headers['Content-Disposition']).to eq(%{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
expect(artifact_request.response_headers['Content-Transfer-Encoding']).to eq("binary")
expect(artifact_request.response_headers['Content-Type']).to eq("image/gif")
expect(artifact_request.body).to eq(job.artifacts_file.file.read.b)
diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
index 6656ca3ef18..2780326cd35 100644
--- a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Group member cannot request access to their group project',
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index 9db34cee5d6..47cd0d612b5 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Group requester cannot request access to project', :js,
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 6f76424e377..9af36b4b2a9 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -38,9 +38,11 @@ RSpec.describe 'Projects > Members > User requests access', :js, feature_categor
context 'code access is restricted' do
it 'user can request access' do
- project.project_feature.update!(repository_access_level: ProjectFeature::PRIVATE,
- builds_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(
+ repository_access_level: ProjectFeature::PRIVATE,
+ builds_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE
+ )
visit project_path(project)
expect(page).to have_content 'Request Access'
diff --git a/spec/features/projects/milestones/gfm_autocomplete_spec.rb b/spec/features/projects/milestones/gfm_autocomplete_spec.rb
index d4ce10b5cb5..0705cdd0d9e 100644
--- a/spec/features/projects/milestones/gfm_autocomplete_spec.rb
+++ b/spec/features/projects/milestones/gfm_autocomplete_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
+ include Features::AutocompleteHelpers
+
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:project) { create(:project, :repository, group: group) }
@@ -68,10 +70,6 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
private
- def find_autocomplete_menu
- find('.atwho-view ul', visible: true)
- end
-
def expect_autocomplete_entry(entry)
page.within('.atwho-container') do
expect(page).to have_content(entry)
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 97dfeb6fd06..b6645e9b710 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -15,8 +15,6 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
before do
sign_in(user)
- stub_feature_flags(show_pages_in_deployments_menu: false)
-
stub_config(registry: { enabled: false })
stub_feature_flags(harbor_registry_integration: false)
stub_feature_flags(ml_experiment_tracking: false)
@@ -53,8 +51,8 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
- _('Packages and registries'),
- within: _('Settings'),
+ _('Releases'),
+ within: _('Deployments'),
new_sub_nav_item_name: _('Pages')
)
diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb
index 708210e669c..ae459197b38 100644
--- a/spec/features/projects/pages/user_adds_domain_spec.rb
+++ b/spec/features/projects/pages/user_adds_domain_spec.rb
@@ -178,7 +178,7 @@ RSpec.describe 'User adds pages domain', :js, feature_category: :pages do
visit project_pages_path(project)
within('#content-body') { click_link 'Edit' }
- expect(page).to have_field :domain_dns, with: "#{domain.domain} ALIAS #{domain.project.pages_subdomain}.#{Settings.pages.host}."
+ expect(page).to have_field :domain_dns, with: "#{domain.domain} ALIAS namespace1.example.com."
end
end
end
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
index 8c713b6f73a..eec9f2befb6 100644
--- a/spec/features/projects/pages/user_edits_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -10,8 +10,6 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- stub_feature_flags(show_pages_in_deployments_menu: false)
-
project.add_maintainer(user)
sign_in(user)
@@ -82,25 +80,39 @@ RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
end
end
- describe 'project settings page' do
- it 'renders "Pages" tab' do
- visit edit_project_path(project)
+ describe 'menu entry' do
+ describe 'on the pages page' do
+ it 'renders "Pages" tab' do
+ visit project_pages_path(project)
- page.within '.nav-sidebar' do
- expect(page).to have_link('Pages')
+ page.within '.nav-sidebar' do
+ expect(page).to have_link('Pages')
+ end
end
end
- context 'when pages are disabled' do
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ describe 'in another menu entry under deployments' do
+ context 'when pages are enabled' do
+ it 'renders "Pages" tab' do
+ visit project_environments_path(project)
+
+ page.within '.nav-sidebar' do
+ expect(page).to have_link('Pages')
+ end
+ end
end
- it 'does not render "Pages" tab' do
- visit edit_project_path(project)
+ context 'when pages are disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
- page.within '.nav-sidebar' do
- expect(page).not_to have_link('Pages')
+ it 'does not render "Pages" tab' do
+ visit project_environments_path(project)
+
+ page.within '.nav-sidebar' do
+ expect(page).not_to have_link('Pages')
+ end
end
end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index abc9e3d30fc..bb49fb734d7 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -13,7 +13,6 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
let(:role) { :developer }
before do
- stub_feature_flags(pipeline_details_header_vue: false)
sign_in(user)
project.add_role(user, role)
end
@@ -22,42 +21,39 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
let!(:external_stage) { create(:ci_stage, name: 'external', pipeline: pipeline) }
let!(:build_passed) do
- create(:ci_build, :success,
- pipeline: pipeline, stage: 'build', stage_idx: 0, name: 'build')
+ create(:ci_build, :success, pipeline: pipeline, stage: 'build', stage_idx: 0, name: 'build')
end
let!(:build_failed) do
- create(:ci_build, :failed,
- pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'test')
+ create(:ci_build, :failed, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'test')
end
let!(:build_preparing) do
- create(:ci_build, :preparing,
- pipeline: pipeline, stage: 'deploy', stage_idx: 2, name: 'prepare')
+ create(:ci_build, :preparing, pipeline: pipeline, stage: 'deploy', stage_idx: 2, name: 'prepare')
end
let!(:build_running) do
- create(:ci_build, :running,
- pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'deploy')
+ create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'deploy')
end
let!(:build_manual) do
- create(:ci_build, :manual,
- pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'manual-build')
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'manual-build')
end
let!(:build_scheduled) do
- create(:ci_build, :scheduled,
- pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'delayed-job')
+ create(:ci_build, :scheduled, pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'delayed-job')
end
let!(:build_external) do
- create(:generic_commit_status, status: 'success',
- pipeline: pipeline,
- name: 'jenkins',
- ci_stage: external_stage,
- ref: 'master',
- target_url: 'http://gitlab.com/status')
+ create(
+ :generic_commit_status,
+ status: 'success',
+ pipeline: pipeline,
+ name: 'jenkins',
+ ci_stage: external_stage,
+ ref: 'master',
+ target_url: 'http://gitlab.com/status'
+ )
end
end
@@ -93,9 +89,9 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the pipeline information' do
visit_pipeline
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for #{pipeline.ref}")
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_content("For #{pipeline.ref}")
+ expect(page).to have_content("#{pipeline.statuses.count} Jobs")
expect(page).to have_link(pipeline.ref,
href: project_commits_path(pipeline.project, pipeline.ref))
end
@@ -104,135 +100,63 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'displays pipeline name instead of commit title' do
visit_pipeline
- within 'h3' do
+ within '[data-testid="pipeline-details-header"]' do
expect(page).to have_content(pipeline.name)
+ expect(page).to have_content(project.commit.short_id)
+ expect(page).not_to have_selector('[data-testid="pipeline-commit-title"]')
end
+ end
- within '.well-segment[data-testid="commit-row"]' do
- expect(page).to have_content(project.commit.title)
- expect(page).to have_content(project.commit.short_id)
+ context 'without pipeline name' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user)
+ end
+
+ it 'displays commit title' do
+ visit_pipeline
+
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_content(project.commit.title)
+ expect(page).not_to have_selector('[data-testid="pipeline-name"]')
+ end
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)
+ 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)
+ 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
+ it 'shows time ago' 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")
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_selector('[data-testid="pipeline-finished-time-ago"]')
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
- visit_pipeline
-
- within '.related-merge-request-info' do
- expect(page).to have_content('No related merge requests found.')
- end
- end
- end
-
- context 'when there is one related merge request' do
- let!(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: pipeline.ref)
- end
-
- it 'shows a link to the merge request' do
- visit_pipeline
-
- within '.related-merge-requests' do
- expect(page).to have_content('1 related merge request: ')
- expect(page).to have_selector('.js-truncated-mr-list')
- expect(page).to have_link("#{merge_request.to_reference} #{merge_request.title}")
-
- expect(page).not_to have_selector('.js-full-mr-list')
- expect(page).not_to have_selector('.text-expander')
- end
- end
- end
-
- context 'when there are two related merge requests' do
- let!(:merge_request1) do
- create(:merge_request,
- source_project: project,
- source_branch: pipeline.ref)
- end
-
- let!(:merge_request2) do
- create(:merge_request,
- source_project: project,
- source_branch: pipeline.ref,
- target_branch: 'fix')
- end
-
- it 'links to the most recent related merge request' do
- visit_pipeline
-
- within '.related-merge-requests' do
- expect(page).to have_content('2 related merge requests: ')
- expect(page).to have_link("#{merge_request2.to_reference} #{merge_request2.title}")
- expect(page).to have_selector('.text-expander')
- expect(page).to have_selector('.js-full-mr-list', visible: false)
- end
- end
-
- it 'expands to show links to all related merge requests' do
+ it 'does not show time ago' do
visit_pipeline
- within '.related-merge-requests' do
- find('.text-expander').click
-
- expect(page).to have_selector('.js-full-mr-list', visible: true)
-
- pipeline.all_merge_requests.map do |merge_request|
- expect(page).to have_link(href: project_merge_request_path(project, merge_request))
- end
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).not_to have_selector('[data-testid="pipeline-finished-time-ago"]')
end
end
end
end
- describe 'pipelines details view' do
- let!(:status) { create(:user_status, user: pipeline.user, emoji: 'smirk', message: 'Authoring this object') }
-
- it 'pipeline header shows the user status and emoji' do
- visit project_pipeline_path(project, pipeline)
-
- within '[data-testid="ci-header-content"]' do
- expect(page).to have_selector("[data-testid='#{status.message}']")
- expect(page).to have_selector("[data-name='#{status.emoji}']")
- end
- end
- end
-
describe 'pipeline graph' do
context 'when pipeline has running builds' do
before do
@@ -241,7 +165,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
- expect(page).to have_selector('.js-ci-status-icon-running')
+ expect(page).to have_selector('[data-testid="status_running-icon"]')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('deploy')
end
@@ -263,7 +187,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows a preparing icon and a cancel action' do
page.within('#ci-badge-prepare') do
- expect(page).to have_selector('.js-ci-status-icon-preparing')
+ expect(page).to have_selector('[data-testid="status_preparing-icon"]')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('prepare')
end
@@ -285,7 +209,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
- expect(page).to have_selector('.js-ci-status-icon-success')
+ expect(page).to have_selector('[data-testid="status_success-icon"]')
expect(page).to have_content('build')
end
@@ -299,8 +223,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
wait_for_requests
expect(page).not_to have_content('Retry job')
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
+ within('[data-testid="pipeline-details-header"]') do
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
end
end
@@ -314,7 +238,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the scheduled icon and an unschedule action for the delayed job' do
page.within('#ci-badge-delayed-job') do
- expect(page).to have_selector('.js-ci-status-icon-scheduled')
+ expect(page).to have_selector('[data-testid="status_scheduled-icon"]')
expect(page).to have_content('delayed-job')
end
@@ -339,7 +263,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the failed icon and a retry action for the failed build' do
page.within('#ci-badge-test') do
- expect(page).to have_selector('.js-ci-status-icon-failed')
+ expect(page).to have_selector('[data-testid="status_failed-icon"]')
expect(page).to have_content('test')
end
@@ -353,8 +277,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
wait_for_requests
expect(page).not_to have_content('Retry job')
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
+ within('[data-testid="pipeline-details-header"]') do
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
end
@@ -373,7 +297,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('.js-ci-status-icon-manual')
+ expect(page).to have_selector('[data-testid="status_manual-icon"]')
expect(page).to have_content('manual')
end
@@ -387,8 +311,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
wait_for_requests
expect(page).not_to have_content('Play job')
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
+ within('[data-testid="pipeline-details-header"]') do
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
end
end
@@ -399,7 +323,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('.js-ci-status-icon-success')
+ expect(page).to have_selector('[data-testid="status_success-icon"]')
expect(page).to have_content('jenkins')
expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
@@ -408,13 +332,15 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
context 'when pipeline has a downstream pipeline' do
let(:downstream_project) { create(:project, :repository, group: group) }
let(:downstream_pipeline) do
- create(:ci_pipeline,
- status,
- user: user,
- project: downstream_project,
- ref: 'master',
- sha: downstream_project.commit.id,
- child_of: pipeline)
+ create(
+ :ci_pipeline,
+ status,
+ user: user,
+ project: downstream_project,
+ ref: 'master',
+ sha: downstream_project.commit.id,
+ child_of: pipeline
+ )
end
let!(:build) { create(:ci_build, status, pipeline: downstream_pipeline, user: user) }
@@ -601,7 +527,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
context 'when retrying' do
before do
- find('[data-testid="retryPipeline"]').click
+ find('[data-testid="retry-pipeline"]').click
wait_for_requests
end
@@ -610,8 +536,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'shows running status in pipeline header', :sidekiq_might_not_need_inline do
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
+ within('[data-testid="pipeline-details-header"]') do
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
end
end
@@ -661,10 +587,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
context 'when pipeline ref does not exist in repository anymore' do
let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- ref: 'non-existent',
- sha: project.commit.id,
- user: user)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'non-existent',
+ sha: project.commit.id,
+ user: user
+ )
end
before do
@@ -677,7 +606,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'does not render render raw HTML to the pipeline ref' do
- page.within '.pipeline-info' do
+ page.within '[data-testid="pipeline-details-header"]' do
expect(page).not_to have_content('<span class="ref-name"')
end
end
@@ -688,10 +617,12 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
let(:target_project) { project }
let(:merge_request) do
- create(:merge_request,
+ create(
+ :merge_request,
:with_detached_merge_request_pipeline,
source_project: source_project,
- target_project: target_project)
+ target_project: target_project
+ )
end
let(:pipeline) do
@@ -701,10 +632,10 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the pipeline information' do
visit_pipeline
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch}")
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_content("#{pipeline.statuses.count} Jobs")
+ expect(page).to have_content("Related merge request !#{merge_request.iid} " \
+ "to merge #{merge_request.source_branch}")
expect(page).to have_link("!#{merge_request.iid}",
href: project_merge_request_path(project, merge_request))
expect(page).to have_link(merge_request.source_branch,
@@ -720,7 +651,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'does not link to the source branch commit path' do
visit_pipeline
- within '.pipeline-info' do
+ within '[data-testid="pipeline-details-header"]' do
expect(page).not_to have_link(merge_request.source_branch)
expect(page).to have_content(merge_request.source_branch)
end
@@ -735,10 +666,10 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'shows the pipeline information', :sidekiq_might_not_need_inline do
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch}")
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_content("#{pipeline.statuses.count} Jobs")
+ expect(page).to have_content("Related merge request !#{merge_request.iid} " \
+ "to merge #{merge_request.source_branch}")
expect(page).to have_link("!#{merge_request.iid}",
href: project_merge_request_path(project, merge_request))
expect(page).to have_link(merge_request.source_branch,
@@ -772,10 +703,10 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows the pipeline information' do
visit_pipeline
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch} " \
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_content("#{pipeline.statuses.count} Jobs")
+ expect(page).to have_content("Related merge request !#{merge_request.iid} " \
+ "to merge #{merge_request.source_branch} " \
"into #{merge_request.target_branch}")
expect(page).to have_link("!#{merge_request.iid}",
href: project_merge_request_path(project, merge_request))
@@ -794,7 +725,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'does not link to the target branch commit path' do
visit_pipeline
- within '.pipeline-info' do
+ within '[data-testid="pipeline-details-header"]' do
expect(page).not_to have_link(merge_request.target_branch)
expect(page).to have_content(merge_request.target_branch)
end
@@ -809,10 +740,10 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'shows the pipeline information', :sidekiq_might_not_need_inline do
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch} " \
+ within '[data-testid="pipeline-details-header"]' do
+ expect(page).to have_content("#{pipeline.statuses.count} Jobs")
+ expect(page).to have_content("Related merge request !#{merge_request.iid} " \
+ "to merge #{merge_request.source_branch} " \
"into #{merge_request.target_branch}")
expect(page).to have_link("!#{merge_request.iid}",
href: project_merge_request_path(project, merge_request))
@@ -864,17 +795,23 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
let(:downstream) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_pipeline, project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user
+ )
end
let!(:bridge) do
- create(:ci_bridge, pipeline: pipeline,
- name: 'cross-build',
- user: user,
- downstream: downstream)
+ create(
+ :ci_bridge,
+ pipeline: pipeline,
+ name: 'cross-build',
+ user: user,
+ downstream: downstream
+ )
end
describe 'GET /:project/-/pipelines/:id' do
@@ -942,13 +879,20 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
let(:resource_group) { create(:ci_resource_group, project: project) }
let!(:test_job) do
- create(:ci_build, :pending, stage: 'test', name: 'test',
- stage_idx: 1, pipeline: pipeline, project: project)
+ create(:ci_build, :pending, stage: 'test', name: 'test', stage_idx: 1, pipeline: pipeline, project: project)
end
let!(:deploy_job) do
- create(:ci_build, :created, stage: 'deploy', name: 'deploy',
- stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ create(
+ :ci_build,
+ :created,
+ stage: 'deploy',
+ name: 'deploy',
+ stage_idx: 2,
+ pipeline: pipeline,
+ project: project,
+ resource_group: resource_group
+ )
end
describe 'GET /:project/-/pipelines/:id' do
@@ -957,7 +901,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows deploy job as created' do
subject
- within('.js-pipeline-header-container') do
+ within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content('pending')
end
@@ -982,7 +926,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows deploy job as pending' do
subject
- within('.js-pipeline-header-container') do
+ within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content('running')
end
@@ -1011,7 +955,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows deploy job as waiting for resource' do
subject
- within('.js-pipeline-header-container') do
+ within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content('waiting')
end
@@ -1031,7 +975,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows deploy job as pending' do
subject
- within('.js-pipeline-header-container') do
+ within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content('running')
end
@@ -1059,7 +1003,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'shows deploy job as waiting for resource' do
subject
- within('.js-pipeline-header-container') do
+ within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content('waiting')
end
@@ -1207,8 +1151,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
context 'when user does have permission to retry build' do
before do
- create(:protected_branch, :developers_can_merge,
- name: pipeline.ref, project: project)
+ create(:protected_branch, :developers_can_merge, name: pipeline.ref, project: project)
end
it 'shows retry button for failed build' do
@@ -1315,11 +1258,13 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
include_context 'pipeline builds'
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user
+ )
end
before do
@@ -1327,7 +1272,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'contains badge that indicates it is the latest build' do
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content 'latest'
end
end
@@ -1335,12 +1280,14 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
context 'when pipeline has configuration errors' do
let(:pipeline) do
- create(:ci_pipeline,
- :invalid,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
+ create(
+ :ci_pipeline,
+ :invalid,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user
+ )
end
before do
@@ -1348,7 +1295,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'contains badge that indicates errors' do
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content 'yaml invalid'
end
end
@@ -1356,9 +1303,9 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'contains badge with tooltip which contains error' do
expect(pipeline).to have_yaml_errors
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_selector(
- %Q{span[title="#{pipeline.yaml_errors}"]})
+ %{span[title="#{pipeline.yaml_errors}"]})
end
end
@@ -1369,26 +1316,16 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
it 'contains badge with tooltip which contains failure reason' do
expect(pipeline.failure_reason?).to eq true
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_selector(
- %Q{span[title="#{pipeline.present.failure_reason}"]})
+ %{span[title="#{pipeline.present.failure_reason}"]})
end
end
-
- it 'contains a pipeline header with title' do
- expect(page).to have_content "Pipeline ##{pipeline.id}"
- end
end
context 'when pipeline is stuck' do
- include_context 'pipeline builds'
-
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
+ create(:ci_pipeline, project: project, status: :created, user: user)
end
before do
@@ -1397,7 +1334,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'contains badge that indicates being stuck' do
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content 'stuck'
end
end
@@ -1408,12 +1345,14 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
let(:project) { create(:project, :repository, auto_devops_attributes: { enabled: true }) }
let(:pipeline) do
- create(:ci_pipeline,
- :auto_devops_source,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
+ create(
+ :ci_pipeline,
+ :auto_devops_source,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user
+ )
end
before do
@@ -1421,7 +1360,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'contains badge that indicates using auto devops' do
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content 'Auto DevOps'
end
end
@@ -1431,21 +1370,25 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
include_context 'pipeline builds'
let(:pipeline) do
- create(:ci_pipeline,
- source: :merge_request_event,
- project: merge_request.source_project,
- ref: 'feature',
- sha: merge_request.diff_head_sha,
- user: user,
- merge_request: merge_request)
+ create(
+ :ci_pipeline,
+ source: :merge_request_event,
+ project: merge_request.source_project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ user: user,
+ merge_request: merge_request
+ )
end
let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: 'feature',
- target_project: project,
- target_branch: 'master')
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master'
+ )
end
before do
@@ -1453,7 +1396,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
end
it 'contains badge that indicates detached merge request pipeline' do
- page.within(all('.well-segment')[1]) do
+ page.within('[data-testid="pipeline-details-header"]') do
expect(page).to have_content 'merge request'
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 441f39e6999..25eddf64f99 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
+RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
include ListboxHelpers
include ProjectForksHelper
include Spec::Support::Helpers::ModalHelpers
@@ -10,10 +10,6 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
let(:project) { create(:project) }
let(:expected_detached_mr_tag) { 'merge request' }
- before do
- stub_feature_flags(pipeline_details_header_vue: false)
- end
-
context 'when user is logged in' do
let(:user) { create(:user) }
@@ -109,8 +105,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
context 'when pipeline is cancelable' do
let!(:build) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ create(:ci_build, pipeline: pipeline, stage: 'test')
end
before do
@@ -139,8 +134,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
context 'when pipeline is retryable', :sidekiq_might_not_need_inline do
let!(:build) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
+ create(:ci_build, pipeline: pipeline, stage: 'test')
end
before do
@@ -168,10 +162,12 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
context 'when pipeline is detached merge request pipeline' do
let(:merge_request) do
- create(:merge_request,
- :with_detached_merge_request_pipeline,
- source_project: source_project,
- target_project: target_project)
+ create(
+ :merge_request,
+ :with_detached_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project
+ )
end
let!(:pipeline) { merge_request.all_pipelines.first }
@@ -187,8 +183,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
within '.pipeline-tags' do
expect(page).to have_content(expected_detached_mr_tag)
- expect(page).to have_link(merge_request.iid,
- href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.iid, href: project_merge_request_path(project, merge_request))
expect(page).not_to have_link(pipeline.ref)
end
@@ -206,11 +201,13 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
context 'when pipeline is merge request pipeline' do
let(:merge_request) do
- create(:merge_request,
- :with_merge_request_pipeline,
- source_project: source_project,
- target_project: target_project,
- merge_sha: target_project.commit.sha)
+ create(
+ :merge_request,
+ :with_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project,
+ merge_sha: target_project.commit.sha
+ )
end
let!(:pipeline) { merge_request.all_pipelines.first }
@@ -226,8 +223,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
within '.pipeline-tags' do
expect(page).not_to have_content(expected_detached_mr_tag)
- expect(page).to have_link(merge_request.iid,
- href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.iid, href: project_merge_request_path(project, merge_request))
expect(page).not_to have_link(pipeline.ref)
end
@@ -259,7 +255,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'contains badge with tooltip which contains error' do
expect(pipeline).to have_yaml_errors
expect(page).to have_selector(
- %Q{span[title="#{pipeline.yaml_errors}"]})
+ %{span[title="#{pipeline.yaml_errors}"]})
end
it 'contains badge that indicates failure reason' do
@@ -269,7 +265,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'contains badge with tooltip which contains failure reason' do
expect(pipeline.failure_reason?).to eq true
expect(page).to have_selector(
- %Q{span[title="#{pipeline.present.failure_reason}"]})
+ %{span[title="#{pipeline.present.failure_reason}"]})
end
end
@@ -524,9 +520,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
context 'mini pipeline graph' do
let!(:build) do
- create(:ci_build, :pending, pipeline: pipeline,
- stage: 'build',
- name: 'build')
+ create(:ci_build, :pending, pipeline: pipeline, stage: 'build', name: 'build')
end
dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
@@ -558,9 +552,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
context 'for a failed pipeline' do
let!(:build) do
- create(:ci_build, :failed, pipeline: pipeline,
- stage: 'build',
- name: 'build')
+ create(:ci_build, :failed, pipeline: pipeline, stage: 'build', name: 'build')
end
it 'displays the failure reason' do
@@ -628,10 +620,12 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
let(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_empty_pipeline,
- project: project,
- sha: project.commit.id,
- user: user)
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: project.commit.id,
+ user: user
+ )
end
let(:external_stage) { create(:ci_stage, name: 'external', pipeline: pipeline) }
@@ -656,7 +650,6 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
# header
expect(page).to have_text("##{pipeline.id}")
- expect(page).to have_selector(%Q(img[src="#{pipeline.user.avatar_url}"]))
expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
# stages
diff --git a/spec/features/projects/releases/user_views_release_spec.rb b/spec/features/projects/releases/user_views_release_spec.rb
index efa0ebd761d..282b8958814 100644
--- a/spec/features/projects/releases/user_views_release_spec.rb
+++ b/spec/features/projects/releases/user_views_release_spec.rb
@@ -7,10 +7,12 @@ RSpec.describe 'User views Release', :js, feature_category: :continuous_delivery
let(:user) { create(:user) }
let(:release) do
- create(:release,
- project: project,
- name: 'The first release',
- description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)')
+ create(
+ :release,
+ project: project,
+ name: 'The first release',
+ description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)'
+ )
end
before do
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index a38c10c6bab..210815f341c 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :use
end
context 'when token creation is not allowed' do
- it_behaves_like 'resource access tokens creation disallowed', 'Project access token creation is disabled in this group. You can still use and manage existing tokens.'
+ it_behaves_like 'resource access tokens creation disallowed', 'Project access token creation is disabled in this group.'
context 'with a project in a personal namespace' do
let(:personal_project) { create(:project) }
diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
index 4a56e6c8bbf..4214e6fc767 100644
--- a/spec/features/projects/settings/external_authorization_service_settings_spec.rb
+++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Settings > External Authorization Classification Label setting',
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) }
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index c5a5826a778..b46451f4255 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -18,8 +18,11 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego
visit project_path(project)
wait_for_requests
- expect(page).to have_selector('.sidebar-sub-level-items a[aria-label="Error Tracking"]',
- text: 'Error Tracking', visible: :hidden)
+ expect(page).to have_selector(
+ '.sidebar-sub-level-items a[aria-label="Error Tracking"]',
+ text: 'Error Tracking',
+ visible: :hidden
+ )
end
end
diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
index 50693dda685..1ab88ec0fff 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index b8016a5d2df..9df82e447aa 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 2439e624dd6..d53aefe5a4e 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe 'Projects > Settings > Repository settings', feature_category: :g
let(:role) { :developer }
before do
- stub_feature_flags(mirror_only_branches_match_regex: false)
project.add_role(user, role)
sign_in(user)
end
diff --git a/spec/features/projects/settings/service_desk_setting_spec.rb b/spec/features/projects/settings/service_desk_setting_spec.rb
index c18da56f3ee..d068cb219f1 100644
--- a/spec/features/projects/settings/service_desk_setting_spec.rb
+++ b/spec/features/projects/settings/service_desk_setting_spec.rb
@@ -2,18 +2,20 @@
require 'spec_helper'
-RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_category: :groups_and_projects do
- let(:project) { create(:project_empty_repo, :private, service_desk_enabled: false) }
+RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_category: :service_desk do
+ let_it_be_with_reload(:project) { create(:project_empty_repo, :private, service_desk_enabled: false) }
let(:presenter) { project.present(current_user: user) }
- let(:user) { create(:user) }
+ let_it_be_with_reload(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
- allow_any_instance_of(Project).to receive(:present).with(current_user: user).and_return(presenter)
- allow(::Gitlab::Email::IncomingEmail).to receive(:enabled?) { true }
- allow(::Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?) { true }
+ allow_next_instance_of(Project) do |project|
+ allow(project).to receive(:present).with(current_user: user).and_return(presenter)
+ end
+ allow(::Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)
+ allow(::Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
end
it 'shows activation checkbox' do
@@ -43,8 +45,8 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
context 'when service_desk_email is enabled' do
before do
- allow(::Gitlab::Email::ServiceDeskEmail).to receive(:enabled?) { true }
- allow(::Gitlab::Email::ServiceDeskEmail).to receive(:address_for_key) { 'address-suffix@example.com' }
+ allow(::Gitlab::Email::ServiceDeskEmail).to receive(:enabled?).and_return(true)
+ allow(::Gitlab::Email::ServiceDeskEmail).to receive(:address_for_key).and_return('address-suffix@example.com')
visit edit_project_path(project)
end
@@ -66,7 +68,7 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
expect(find('[data-testid="incoming-email"]').value).to eq('address-suffix@example.com')
end
- context 'issue description templates' do
+ describe 'issue description templates' do
let_it_be(:issuable_project_template_files) do
{
'.gitlab/issue_templates/project-issue-bar.md' => 'Project Issue Template Bar',
@@ -82,8 +84,13 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
end
let_it_be_with_reload(:group) { create(:group) }
- let_it_be_with_reload(:project) { create(:project, :custom_repo, group: group, files: issuable_project_template_files) }
- let_it_be(:group_template_repo) { create(:project, :custom_repo, group: group, files: issuable_group_template_files) }
+ let_it_be_with_reload(:project) do
+ create(:project, :custom_repo, group: group, files: issuable_project_template_files)
+ end
+
+ let_it_be(:group_template_repo) do
+ create(:project, :custom_repo, group: group, files: issuable_group_template_files)
+ end
before do
stub_licensed_features(custom_file_templates_for_namespace: false, custom_file_templates: false)
@@ -94,4 +101,10 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
it_behaves_like 'issue description templates from current project only'
end
end
+
+ it 'pushes service_desk_custom_email feature flag to frontend' do
+ visit edit_project_path(project)
+
+ expect(page).to have_pushed_frontend_feature_flags(serviceDeskCustomEmail: true)
+ end
end
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index f0ef4a285ad..978b678c334 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -26,14 +26,6 @@ RSpec.describe 'User searches project settings', :js, feature_category: :groups_
it_behaves_like 'can highlight results', 'third-party applications'
end
- context 'in Webhooks page' do
- before do
- visit project_hooks_path(project)
- end
-
- it_behaves_like 'can highlight results', 'Secret token'
- end
-
context 'in Access Tokens page' do
before do
visit project_settings_access_tokens_path(project)
@@ -65,15 +57,4 @@ RSpec.describe 'User searches project settings', :js, feature_category: :groups_
it_behaves_like 'can search settings', 'Alerts', 'Error tracking'
end
-
- context 'in Pages page' do
- before do
- stub_feature_flags(show_pages_in_deployments_menu: false)
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
-
- visit project_pages_path(project)
- end
-
- it_behaves_like 'can highlight results', 'static website'
- end
end
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index 5d345c63d60..af7c790c692 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :grou
it 'show list of webhooks' do
hook
-
visit webhooks_path
expect(page.status_code).to eq(200)
@@ -46,11 +45,13 @@ RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :grou
expect(page).to have_content('Pipeline events')
expect(page).to have_content('Wiki page events')
expect(page).to have_content('Releases events')
+ expect(page).to have_content('Emoji events')
end
it 'create webhook', :js do
visit webhooks_path
+ click_button 'Add new webhook'
fill_in 'URL', with: url
check 'Tag push events'
check 'Enable SSL verification'
@@ -59,10 +60,10 @@ RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :grou
click_button 'Add webhook'
expect(page).to have_content(url)
+ expect(page).to have_content('Webhook was created')
expect(page).to have_content('SSL Verification: enabled')
expect(page).to have_content('Tag push events')
expect(page).to have_content('Job events')
- expect(page).to have_content('Push events')
end
it 'edit existing webhook', :js do
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index a4df6a56e02..8e27b4b2ede 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -9,18 +9,24 @@ RSpec.describe 'Projects > Show > Download buttons', feature_category: :groups_a
let(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit.sha,
- ref: project.default_branch,
- status: status)
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status
+ )
end
let!(:build) do
- create(:ci_build, :success, :artifacts,
- pipeline: pipeline,
- status: pipeline.status,
- name: 'build')
+ create(
+ :ci_build,
+ :success,
+ :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build'
+ )
end
before do
diff --git a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb
index 997a804e6ac..98714da34f2 100644
--- a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb
+++ b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Project > Show > User interacts with auto devops implicitly enabled banner',
-feature_category: :groups_and_projects do
+ feature_category: :groups_and_projects do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index 29fb20841fd..ee017336acc 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -43,8 +43,8 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New file')
- expect(page).to have_link('Upload file')
- expect(page).to have_link('New directory')
+ expect(page).to have_button('Upload file')
+ expect(page).to have_button('New directory')
expect(page).to have_link('New branch')
expect(page).to have_link('New tag')
end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index 570721fc951..275d364f267 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -10,18 +10,22 @@ RSpec.describe 'Download buttons in tags page', feature_category: :source_code_m
let(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit(tag).sha,
- ref: tag,
- status: status)
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit(tag).sha,
+ ref: tag,
+ status: status
+ )
end
let!(:build) do
- create(:ci_build, :success, :artifacts,
- pipeline: pipeline,
- status: pipeline.status,
- name: 'build')
+ create(
+ :ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build'
+ )
end
before do
diff --git a/spec/features/projects/user_sees_user_popover_spec.rb b/spec/features/projects/user_sees_user_popover_spec.rb
index 523f1366a14..be7d3daa24f 100644
--- a/spec/features/projects/user_sees_user_popover_spec.rb
+++ b/spec/features/projects/user_sees_user_popover_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'User sees user popover', :js, feature_category: :groups_and_proj
end
it 'displays user popover' do
- find('.js-user-link').hover
+ find('.detail-page-description .js-user-link').hover
expect(page).to have_css(popover_selector, visible: true)
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index 79744633d0c..9a772ff8e44 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -9,11 +9,13 @@ RSpec.describe 'Projects > Wiki > User views wiki in project page', feature_cate
context 'when repository is disabled for project' do
let(:project) do
- create(:project,
- :wiki_repo,
- :repository_disabled,
- :merge_requests_disabled,
- :builds_disabled)
+ create(
+ :project,
+ :wiki_repo,
+ :repository_disabled,
+ :merge_requests_disabled,
+ :builds_disabled
+ )
end
context 'when wiki homepage contains a link' do
@@ -37,8 +39,13 @@ RSpec.describe 'Projects > Wiki > User views wiki in project page', feature_cate
context 'when using asciidoc' do
before do
- create(:wiki_page, wiki: project.wiki, title: 'home', content: 'link:other-page[some link]',
- format: :asciidoc)
+ create(
+ :wiki_page,
+ wiki: project.wiki,
+ title: 'home',
+ content: 'link:other-page[some link]',
+ format: :asciidoc
+ )
end
it_behaves_like 'wiki homepage contains a link'
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 4df9109875e..3c63ec82778 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -14,17 +14,11 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
stub_feature_flags(project_runners_vue_ui: false)
end
- context 'when user views runners page' do
- let_it_be(:project) { create(:project) }
-
- before do
- project.add_maintainer(user)
- end
+ context 'with user as project maintainer' do
+ let_it_be(:project) { create(:project).tap { |project| project.add_maintainer(user) } }
- context 'when create_runner_workflow_for_namespace is enabled', :js do
+ context 'when user views runners page', :js do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
-
visit project_runners_path(project)
end
@@ -38,58 +32,18 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
end
end
- context 'when user views new runner page' do
- context 'when create_runner_workflow_for_namespace is enabled', :js do
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
-
- visit new_project_runner_path(project)
- end
-
- it_behaves_like 'creates runner and shows register page' do
- let(:register_path_pattern) { register_project_runner_path(project, '.*') }
- end
-
- it 'shows the locked field' do
- expect(page).to have_selector('input[type="checkbox"][name="locked"]')
- expect(page).to have_content(_('Lock to current projects'))
- end
- end
- end
-
- context 'when create_runner_workflow_for_namespace is disabled' do
+ context 'when user views new runner page', :js do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
+ visit new_project_runner_path(project)
end
- it 'user can see a link with instructions on how to install GitLab Runner' do
- visit project_runners_path(project)
-
- expect(page).to have_link('Install GitLab Runner and ensure it\'s running.', href: "https://docs.gitlab.com/runner/install/")
+ it_behaves_like 'creates runner and shows register page' do
+ let(:register_path_pattern) { register_project_runner_path(project, '.*') }
end
- describe 'runners registration token' do
- let!(:token) { project.runners_token }
-
- before do
- visit project_runners_path(project)
- end
-
- it 'has a registration token' do
- expect(page.find('#registration_token')).to have_content(token)
- end
-
- describe 'reload registration token' do
- let(:page_token) { find('#registration_token').text }
-
- before do
- click_link 'Reset registration token'
- end
-
- it 'changes registration token' do
- expect(page_token).not_to eq token
- end
- end
+ it 'shows the locked field' do
+ expect(page).to have_selector('input[type="checkbox"][name="locked"]')
+ expect(page).to have_content(_('Lock to current projects'))
end
end
end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 238e59be940..7ca7958f61b 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting,
-feature_category: :global_search do
+ feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:milestone1) { create(:milestone, title: 'Foo', project: project) }
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 1d8bdc58ce6..65f262075f9 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting,
-feature_category: :global_search do
+ feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let_it_be(:wiki_page) do
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index 2e3aaab563d..5e553cb0869 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -50,7 +50,9 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search
wait_for_requests
- expect(page).to have_current_path(search_path(search: "test"))
+ expect(page).to have_current_path(search_path, ignore_query: true) do |uri|
+ uri.normalized_query(:sorted) == "scope=blobs&search=test"
+ end
end
end
end
@@ -83,7 +85,9 @@ RSpec.describe 'User uses search filters', :js, feature_category: :global_search
find('[data-testid="project-filter"] [data-testid="clear-icon"]').click
wait_for_requests
- expect(page).to have_current_path(search_path(search: "test"))
+ expect(page).to have_current_path(search_path, ignore_query: true) do |uri|
+ uri.normalized_query(:sorted) == "scope=blobs&search=test"
+ end
end
end
end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index 98842f54015..afb53c563de 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search Snippets', :js, feature_category: :source_code_management do
+RSpec.describe 'Search Snippets', :js, feature_category: :global_search do
it 'user searches for snippets by title' do
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
@@ -11,7 +11,7 @@ RSpec.describe 'Search Snippets', :js, feature_category: :source_code_management
visit dashboard_snippets_path
submit_search('Middle')
- select_search_scope('Titles and Descriptions')
+ select_search_scope(_("Snippets"))
expect(page).to have_link(public_snippet.title)
expect(page).to have_link(private_snippet.title)
diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb
index 0e3f96906de..c2ac33a3b8f 100644
--- a/spec/features/snippets/spam_snippets_spec.rb
+++ b/spec/features/snippets/spam_snippets_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/217722",
- feature_category: :source_code_management do
+ feature_category: :source_code_management do
include_context 'includes Spam constants'
let_it_be(:user) { create(:user) }
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index f4b6b552d46..090d854081a 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -81,8 +81,10 @@ RSpec.describe 'User creates snippet', :js, feature_category: :source_code_manag
context 'when snippets default visibility level is restricted' do
before do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
- default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
+ stub_application_setting(
+ restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
+ default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE
+ )
end
it 'creates a snippet using the lowest available visibility level as default' do
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 8a9d2ff42d9..beadeab1736 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -137,8 +137,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
describe 'multiple tasks' do
let!(:note) do
- create(:note, note: markdown, noteable: issue,
- project: project, author: user)
+ create(:note, note: markdown, noteable: issue, project: project, author: user)
end
it 'renders for note body' do
@@ -171,8 +170,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
describe 'single incomplete task' do
let!(:note) do
- create(:note, note: single_incomplete_markdown, noteable: issue,
- project: project, author: user)
+ create(:note, note: single_incomplete_markdown, noteable: issue, project: project, author: user)
end
it 'renders for note body' do
@@ -186,8 +184,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
describe 'single complete task' do
let!(:note) do
- create(:note, note: single_complete_markdown, noteable: issue,
- project: project, author: user)
+ create(:note, note: single_complete_markdown, noteable: issue, project: project, author: user)
end
it 'renders for note body' do
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
index 02e98905662..2872446ed6b 100644
--- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'User uploads avatar to group', feature_category: :user_profile d
visit group_path(group)
- expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"]))
+ expect(page).to have_selector(%(img[data-src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(group.reload.avatar.file).to exist
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 03b072ea417..cc296259b80 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile
visit user_path(user)
- expect(page).to have_selector(%Q(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
+ expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index aca32d26bdb..9ee3fe846a6 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not_need_inline,
-feature_category: :code_review_workflow do
+ feature_category: :code_review_workflow do
+ include ContentEditorHelpers
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -22,6 +24,8 @@ feature_category: :code_review_workflow do
stub_feature_flags(unbatch_graphql_queries: false)
sign_in(user)
visit(project_merge_request_path(project, merge_request))
+ close_rich_text_promo_popover_if_present
+
page.within('.mr-state-widget') do
click_button 'Merge'
end
@@ -36,6 +40,7 @@ feature_category: :code_review_workflow do
context 'with page reload validates js correctly loaded' do
before do
visit(merge_request_path(merge_request))
+ close_rich_text_promo_popover_if_present
end
it_behaves_like 'showing the revert modal'
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index 481ff52b800..1854e812b73 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -358,10 +358,12 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
def expect_log_message(event = nil, times = 1, reason: '', message: nil)
expect(Gitlab::AppLogger).to have_received(:info)
.exactly(times).times
- .with(message || hash_including(message: 'Email Verification',
- event: event,
- username: user.username,
- ip: '127.0.0.1',
- reason: reason))
+ .with(message || hash_including(
+ message: 'Email Verification',
+ event: event,
+ username: user.username,
+ ip: '127.0.0.1',
+ reason: reason
+ ))
end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 5529f0fa49e..047590fb3aa 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -390,8 +390,12 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
before do
- stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
- providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
+ stub_omniauth_saml_config(
+ enabled: true,
+ auto_link_saml_user: true,
+ allow_single_sign_on: ['saml'],
+ providers: [mock_saml_config_with_upstream_two_factor_authn_contexts]
+ )
end
it 'displays the remember me checkbox' do
@@ -415,8 +419,10 @@ 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
@@ -991,8 +997,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
context 'when the user already enabled 2FA' do
before do
- user.update!(otp_required_for_login: true,
- otp_secret: User.generate_otp_secret(32))
+ user.update!(otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
end
it 'asks the user to accept the terms' do
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index ff903358931..fdd0c38a718 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -9,12 +9,14 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
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
before do
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index bc37c9941ce..39b6d049e43 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -6,6 +6,10 @@ RSpec.describe 'User RSS', feature_category: :user_profile do
let(:user) { create(:user) }
let(:path) { user_path(create(:user)) }
+ before do
+ stub_feature_flags(user_profile_overflow_menu_vue: false)
+ end
+
context 'when signed in' do
before do
sign_in(user)
@@ -22,4 +26,8 @@ RSpec.describe 'User RSS', feature_category: :user_profile do
it_behaves_like "it has an RSS button without a feed token"
end
+
+ # TODO: implement tests before the FF "user_profile_overflow_menu_vue" is turned on
+ # See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
+ # Related Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/416974
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 9c4a1b36ecc..f8653b22377 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -9,10 +9,32 @@ RSpec.describe 'User page', feature_category: :user_profile do
subject(:visit_profile) { visit(user_path(user)) }
- it 'shows user id' do
- subject
+ context 'with "user_profile_overflow_menu_vue" feature flag enabled', :js do
+ it 'does not show the user id in the profile info' do
+ subject
+
+ expect(page).not_to have_content("User ID: #{user.id}")
+ end
+
+ it 'shows copy user id action in the dropdown' do
+ subject
+
+ find('[data-testid="base-dropdown-toggle"').click
+
+ expect(page).to have_content("Copy user ID: #{user.id}")
+ end
+ end
+
+ context 'with "user_profile_overflow_menu_vue" feature flag disabled', :js do
+ before do
+ stub_feature_flags(user_profile_overflow_menu_vue: false)
+ end
+
+ it 'shows user id' do
+ subject
- expect(page).to have_content("User ID: #{user.id}")
+ expect(page).to have_content("User ID: #{user.id}")
+ end
end
it 'shows name on breadcrumbs' do
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index 5c61843e558..cf62ccaf999 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -41,6 +41,21 @@ RSpec.describe 'Users > Terms', :js, feature_category: :user_profile do
end
end
+ context 'when user is a service account' do
+ let(:service_account) { create(:user, :service_account) }
+
+ before do
+ enforce_terms
+ end
+
+ it 'auto accepts the terms' do
+ visit terms_path
+
+ expect(page).not_to have_content('Accept terms')
+ expect(service_account.terms_accepted?).to be(true)
+ end
+ end
+
context 'when signed in' do
let(:user) { create(:user) }
diff --git a/spec/finders/award_emojis_finder_spec.rb b/spec/finders/award_emojis_finder_spec.rb
index 7a75ad716d0..5c016d9e177 100644
--- a/spec/finders/award_emojis_finder_spec.rb
+++ b/spec/finders/award_emojis_finder_spec.rb
@@ -12,6 +12,10 @@ RSpec.describe AwardEmojisFinder do
let_it_be(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) }
let_it_be(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) }
+ before do
+ stub_feature_flags(custom_emoji: false)
+ end
+
describe 'param validation' do
it 'raises an error if `name` is invalid' do
expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error(
diff --git a/spec/finders/ci/group_variables_finder_spec.rb b/spec/finders/ci/group_variables_finder_spec.rb
new file mode 100644
index 00000000000..9c5d83d7262
--- /dev/null
+++ b/spec/finders/ci/group_variables_finder_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::GroupVariablesFinder, feature_category: :secrets_management do
+ subject(:finder) { described_class.new(project, sort_key).execute }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project_with_subgroup) { create(:project, group: subgroup) }
+ let_it_be(:project_without_group) { create(:project) }
+ let_it_be(:variable1) { create(:ci_group_variable, group: group, key: 'GROUP_VAR_A', created_at: 1.day.ago) }
+ let_it_be(:variable2) { create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B') }
+
+ let_it_be(:inherited_ci_variables) do
+ [variable1, variable2]
+ end
+
+ let(:sort_key) { nil }
+
+ context 'when project does not have a group' do
+ let_it_be(:project) { project_without_group }
+
+ it 'returns an empty array' do
+ expect(finder.to_a).to match_array([])
+ end
+ end
+
+ context 'when project belongs to a group' do
+ let_it_be(:project) { project_with_subgroup }
+
+ it 'returns variable from parent group and ancestors' do
+ expect(finder.to_a).to match_array([variable1, variable2])
+ end
+ end
+
+ describe 'sorting behaviour' do
+ let_it_be(:project) { project_with_subgroup }
+
+ context 'with sort by created_at descending' do
+ let(:sort_key) { :created_desc }
+
+ it 'returns variables ordered by created_at in descending order' do
+ expect(finder.to_a).to eq([variable2, variable1])
+ end
+ end
+
+ context 'with sort by created_at ascending' do
+ let(:sort_key) { :created_asc }
+
+ it 'returns variables ordered by created_at in ascending order' do
+ expect(finder.to_a).to eq([variable1, variable2])
+ end
+ end
+
+ context 'with sort by key descending' do
+ let(:sort_key) { :key_desc }
+
+ it 'returns variables ordered by key in descending order' do
+ expect(finder.to_a).to eq([variable2, variable1])
+ end
+ end
+
+ context 'with sort by key ascending' do
+ let(:sort_key) { :key_asc }
+
+ it 'returns variables ordered by key in ascending order' do
+ expect(finder.to_a).to eq([variable1, variable2])
+ end
+ end
+ end
+end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 77260bb4c5c..e57ad5bc76d 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -302,7 +302,7 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
end
describe '#execute' do
- subject { described_class.new(current_user: user, params: params).execute }
+ subject(:execute) { described_class.new(current_user: user, params: params).execute }
shared_examples 'membership equal to :descendants' do
it 'returns all descendant runners' do
@@ -321,7 +321,7 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
context 'with :group as target group' do
let(:target_group) { group }
- context 'passing no params' do
+ context 'passing no membership params' do
it_behaves_like 'membership equal to :descendants'
end
diff --git a/spec/finders/clusters/agent_tokens_finder_spec.rb b/spec/finders/clusters/agent_tokens_finder_spec.rb
index 1f5bfd58e85..16fdbc1b669 100644
--- a/spec/finders/clusters/agent_tokens_finder_spec.rb
+++ b/spec/finders/clusters/agent_tokens_finder_spec.rb
@@ -47,10 +47,7 @@ RSpec.describe Clusters::AgentTokensFinder do
context 'when filtering by an unrecognised status' do
subject(:execute) { described_class.new(agent, user, status: 'dummy').execute }
- it 'raises an error' do
- # 'dummy' is not a valid status as defined in the AgentToken status enum
- expect { execute.count }.to raise_error(ActiveRecord::StatementInvalid)
- end
+ it { is_expected.to be_empty }
end
context 'when user does not have permission' do
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 517fa0e2c7a..65003ea97ef 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeploymentsFinder do
+RSpec.describe DeploymentsFinder, feature_category: :deployment_management do
subject { described_class.new(params).execute }
describe "validation" do
@@ -166,8 +166,8 @@ RSpec.describe DeploymentsFinder do
'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
'iid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
'iid' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'ref' | 'asc' | [:deployment_1, :deployment_2, :deployment_3] # ref acts like id because of remove_deployments_api_ref_sort feature flag
+ 'ref' | 'desc' | [:deployment_3, :deployment_2, :deployment_1] # ref acts like id because of remove_deployments_api_ref_sort feature flag
'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
'finished_at' | 'asc' | described_class::InefficientQueryError
@@ -185,6 +185,39 @@ RSpec.describe DeploymentsFinder do
end
end
end
+
+ context 'when remove_deployments_api_ref_sort is disabled' do
+ before do
+ stub_feature_flags(remove_deployments_api_ref_sort: false)
+ end
+
+ where(:order_by, :sort, :ordered_deployments) do
+ 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'created_at' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'iid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'iid' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3] # ref sorts when remove_deployments_api_ref_sort feature flag is disabled
+ 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2] # ref sorts when remove_deployments_api_ref_sort feature flag is disabled
+ 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ 'finished_at' | 'asc' | described_class::InefficientQueryError
+ 'finished_at' | 'desc' | described_class::InefficientQueryError
+ 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'iid' | 'err' | [:deployment_1, :deployment_2, :deployment_3]
+ end
+
+ with_them do
+ it 'returns the deployments ordered' do
+ if ordered_deployments == described_class::InefficientQueryError
+ expect { subject }.to raise_error(described_class::InefficientQueryError)
+ else
+ expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
+ end
+ end
+ end
+ end
end
describe 'transform `created_at` sorting to `id` sorting' do
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 9d528355f54..14cbb6a427c 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -276,14 +276,6 @@ RSpec.describe GroupDescendantsFinder do
end
it_behaves_like 'filter examples'
-
- context 'when feature flag :linear_group_descendants_finder_upto is disabled' do
- before do
- stub_feature_flags(linear_group_descendants_finder_upto: false)
- end
-
- it_behaves_like 'filter examples'
- end
end
end
end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index 4189be94cc1..87e579dbeec 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -87,6 +87,16 @@ RSpec.describe GroupProjectsFinder do
end
end
+ context "owned" do
+ before do
+ root_group.add_owner(current_user)
+ end
+
+ let(:params) { { owned: true } }
+
+ it { is_expected.to match_array([private_project, public_project]) }
+ end
+
context "all" do
context 'with subgroups projects' do
before do
diff --git a/spec/finders/packages/ml_model/package_finder_spec.rb b/spec/finders/packages/ml_model/package_finder_spec.rb
new file mode 100644
index 00000000000..535360d13c5
--- /dev/null
+++ b/spec/finders/packages/ml_model/package_finder_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Packages::MlModel::PackageFinder, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package) { create(:ml_model_package, project: project) }
+
+ let(:package_name) { package.name }
+ let(:package_version) { package.version }
+
+ describe '#execute!' do
+ subject(:find_package) { described_class.new(project).execute!(package_name, package_version) }
+
+ it 'finds package by name and version' do
+ expect(find_package).to eq(package)
+ end
+
+ it 'ignores packages with same name but different version' do
+ create(:ml_model_package, project: project, name: package.name, version: '3.1.4')
+
+ expect(find_package).to eq(package)
+ end
+
+ context 'when package name+version does not exist' do
+ let(:package_name) { 'a_package_that_does_not_exist' }
+
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { find_package }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when package exists but is marked for destruction' do
+ let_it_be(:invalid_package) do
+ create(:ml_model_package, project: project, status: :pending_destruction)
+ end
+
+ let(:package_name) { invalid_package.name }
+ let(:package_version) { invalid_package.version }
+
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { find_package }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when package name+version does not exist but it is not ml_model' do
+ let_it_be(:another_package) { create(:generic_package, project: project) }
+
+ let(:package_name) { another_package.name }
+ let(:package_version) { another_package.version }
+
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { find_package }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb
index e11b33f71e9..45cc2a07027 100644
--- a/spec/finders/packages/npm/package_finder_spec.rb
+++ b/spec/finders/packages/npm/package_finder_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe ::Packages::Npm::PackageFinder do
let(:project) { package.project }
let(:package_name) { package.name }
- let(:last_of_each_version) { true }
shared_examples 'accepting a namespace for' do |example_name|
before do
@@ -59,35 +58,11 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
end
- shared_examples 'handling last_of_each_version' do
- include_context 'last_of_each_version setup context'
-
- context 'disabled' do
- let(:last_of_each_version) { false }
-
- it { is_expected.to contain_exactly(package1, package2) }
- end
-
- context 'enabled' do
- it { is_expected.to contain_exactly(package2) }
- end
-
- context 'with npm_allow_packages_in_multiple_projects disabled' do
- before do
- stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
- end
-
- it { is_expected.to contain_exactly(package2) }
- end
- end
-
context 'with a project' do
- let(:finder) { described_class.new(package_name, project: project, last_of_each_version: last_of_each_version) }
+ let(:finder) { described_class.new(package_name, project: project) }
it_behaves_like 'finding packages by name'
- it_behaves_like 'handling last_of_each_version'
-
context 'set to nil' do
let(:project) { nil }
@@ -96,12 +71,10 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
context 'with a namespace' do
- let(:finder) { described_class.new(package_name, namespace: namespace, last_of_each_version: last_of_each_version) }
+ let(:finder) { described_class.new(package_name, namespace: namespace) }
it_behaves_like 'accepting a namespace for', 'finding packages by name'
- it_behaves_like 'accepting a namespace for', 'handling last_of_each_version'
-
context 'set to nil' do
let_it_be(:namespace) { nil }
@@ -125,28 +98,16 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
end
- shared_examples 'handling last_of_each_version' do
- include_context 'last_of_each_version setup context'
-
- context 'enabled' do
- it { is_expected.to eq(package2) }
- end
- end
-
context 'with a project' do
- let(:finder) { described_class.new(package_name, project: project, last_of_each_version: last_of_each_version) }
+ let(:finder) { described_class.new(package_name, project: project) }
it_behaves_like 'finding packages by version'
-
- it_behaves_like 'handling last_of_each_version'
end
context 'with a namespace' do
- let(:finder) { described_class.new(package_name, namespace: namespace, last_of_each_version: last_of_each_version) }
+ let(:finder) { described_class.new(package_name, namespace: namespace) }
it_behaves_like 'accepting a namespace for', 'finding packages by version'
-
- it_behaves_like 'accepting a namespace for', 'handling last_of_each_version'
end
end
@@ -157,26 +118,10 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to eq(package) }
end
- shared_examples 'handling last_of_each_version' do
- include_context 'last_of_each_version setup context'
-
- context 'disabled' do
- let(:last_of_each_version) { false }
-
- it { is_expected.to eq(package2) }
- end
-
- context 'enabled' do
- it { is_expected.to eq(package2) }
- end
- end
-
context 'with a project' do
- let(:finder) { described_class.new(package_name, project: project, last_of_each_version: last_of_each_version) }
+ let(:finder) { described_class.new(package_name, project: project) }
it_behaves_like 'finding package by last'
-
- it_behaves_like 'handling last_of_each_version'
end
context 'with a namespace' do
@@ -184,8 +129,6 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it_behaves_like 'accepting a namespace for', 'finding package by last'
- it_behaves_like 'accepting a namespace for', 'handling last_of_each_version'
-
context 'with duplicate packages' do
let_it_be(:namespace) { create(:group) }
let_it_be(:subgroup1) { create(:group, parent: namespace) }
diff --git a/spec/finders/projects/ml/model_finder_spec.rb b/spec/finders/projects/ml/model_finder_spec.rb
new file mode 100644
index 00000000000..386d690a8d2
--- /dev/null
+++ b/spec/finders/projects/ml/model_finder_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do
+ let_it_be(:model1_a) { create(:ml_model_package) }
+ let_it_be(:project) { model1_a.project }
+ let_it_be(:model1_b) do
+ create(:ml_model_package, name: model1_a.name, project: project)
+ end
+
+ let_it_be(:model2) do
+ create(:ml_model_package, status: :pending_destruction, project: project)
+ end
+
+ let_it_be(:model3) { create(:ml_model_package) }
+ let_it_be(:model4) { create(:generic_package, project: project) }
+
+ subject { described_class.new(project).execute.to_a }
+
+ it 'returns the most recent version of a model' do
+ is_expected.to include(model1_b)
+ end
+
+ it 'does not return older versions of a model' do
+ is_expected.not_to include(model1_a)
+ end
+
+ it 'does not return models pending destruction' do
+ is_expected.not_to include(model2)
+ end
+
+ it 'does not return models belonging to a different project' do
+ is_expected.not_to include(model3)
+ end
+
+ it 'does not return packages that are not ml_model' do
+ is_expected.not_to include(model4)
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 3d108951c64..a795df4dec6 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectsFinder do
+RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do
include AdminModeHelper
describe '#execute' do
@@ -25,6 +25,12 @@ RSpec.describe ProjectsFinder do
create(:project, :private, name: 'D', path: 'D')
end
+ let_it_be(:banned_user_project) do
+ create(:project, :public, name: 'Project created by a banned user', creator: create(:user, :banned)).tap do |p|
+ create(:project_authorization, :owner, user: p.creator, project: p)
+ end
+ end
+
let(:params) { {} }
let(:current_user) { user }
let(:project_ids_relation) { nil }
@@ -488,16 +494,32 @@ RSpec.describe ProjectsFinder do
describe 'with admin user' do
let(:user) { create(:admin) }
- context 'admin mode enabled' do
+ context 'with admin mode enabled' do
before do
enable_admin_mode!(current_user)
end
- it { is_expected.to match_array([public_project, internal_project, private_project, shared_project]) }
+ it do
+ is_expected.to match_array([
+ public_project,
+ internal_project,
+ private_project,
+ shared_project,
+ banned_user_project
+ ])
+ end
end
- context 'admin mode disabled' do
+ context 'with admin mode disabled' do
it { is_expected.to match_array([public_project, internal_project]) }
+
+ context 'when hide_projects_of_banned_users FF is disabled' do
+ before do
+ stub_feature_flags(hide_projects_of_banned_users: false)
+ end
+
+ it { is_expected.to match_array([public_project, internal_project, banned_user_project]) }
+ end
end
end
end
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index e0a9237a79b..c931de92d1c 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -70,19 +70,6 @@ RSpec.describe UsersFinder do
expect(users).to be_empty
end
-
- context 'when autocomplete_users_use_search_service feature flag is disabled' do
- before do
- stub_feature_flags(autocomplete_users_use_search_service: false)
- end
-
- it 'does not pass use_minimum_char_limit from params' do
- search_term = normal_user.username[..1]
- expect(User).to receive(:search).with(search_term, with_private_emails: anything).once.and_call_original
-
- described_class.new(user, { search: search_term, use_minimum_char_limit: false }).execute
- end
- end
end
it 'filters by external users' do
diff --git a/spec/fixtures/api/schemas/public_api/v4/project_hook.json b/spec/fixtures/api/schemas/public_api/v4/project_hook.json
index 6070f3a55f9..b89f5af8078 100644
--- a/spec/fixtures/api/schemas/public_api/v4/project_hook.json
+++ b/spec/fixtures/api/schemas/public_api/v4/project_hook.json
@@ -22,38 +22,106 @@
"releases_events",
"alert_status",
"disabled_until",
- "url_variables"
+ "url_variables",
+ "emoji_events"
],
"properties": {
- "id": { "type": "integer" },
- "project_id": { "type": "integer" },
- "url": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "push_events": { "type": "boolean" },
- "push_events_branch_filter": { "type": ["string", "null"] },
- "tag_push_events": { "type": "boolean" },
- "merge_requests_events": { "type": "boolean" },
- "repository_update_events": { "type": "boolean" },
- "enable_ssl_verification": { "type": "boolean" },
- "issues_events": { "type": "boolean" },
- "confidential_issues_events": { "type": ["boolean", "null"] },
- "note_events": { "type": "boolean" },
- "confidential_note_events": { "type": ["boolean", "null"] },
- "pipeline_events": { "type": "boolean" },
- "wiki_page_events": { "type": "boolean" },
- "job_events": { "type": "boolean" },
- "deployment_events": { "type": "boolean" },
- "releases_events": { "type": "boolean" },
- "alert_status": { "type": "string", "enum": ["executable","disabled","temporarily_disabled"] },
- "disabled_until": { "type": ["string", "null"] },
+ "id": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "url": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "push_events": {
+ "type": "boolean"
+ },
+ "push_events_branch_filter": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tag_push_events": {
+ "type": "boolean"
+ },
+ "merge_requests_events": {
+ "type": "boolean"
+ },
+ "repository_update_events": {
+ "type": "boolean"
+ },
+ "enable_ssl_verification": {
+ "type": "boolean"
+ },
+ "issues_events": {
+ "type": "boolean"
+ },
+ "confidential_issues_events": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "note_events": {
+ "type": "boolean"
+ },
+ "confidential_note_events": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "pipeline_events": {
+ "type": "boolean"
+ },
+ "wiki_page_events": {
+ "type": "boolean"
+ },
+ "job_events": {
+ "type": "boolean"
+ },
+ "deployment_events": {
+ "type": "boolean"
+ },
+ "releases_events": {
+ "type": "boolean"
+ },
+ "emoji_events": {
+ "type": "boolean"
+ },
+ "alert_status": {
+ "type": "string",
+ "enum": [
+ "executable",
+ "disabled",
+ "temporarily_disabled"
+ ]
+ },
+ "disabled_until": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"url_variables": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
- "required": ["key"],
+ "required": [
+ "key"
+ ],
"properties": {
- "key": { "type": "string" }
+ "key": {
+ "type": "string"
+ }
}
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/wiki_blobs.json b/spec/fixtures/api/schemas/public_api/v4/wiki_blobs.json
new file mode 100644
index 00000000000..26379c56a46
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/wiki_blobs.json
@@ -0,0 +1,57 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "basename": {
+ "type": "string"
+ },
+ "data": {
+ "type": "string"
+ },
+ "path": {
+ "type": [
+ "string"
+ ]
+ },
+ "filename": {
+ "type": [
+ "string"
+ ]
+ },
+ "id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "project_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "group_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "ref": {
+ "type": "string"
+ },
+ "startline": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "basename",
+ "data",
+ "path",
+ "filename",
+ "ref",
+ "startline"
+ ],
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/slack/manifest.json b/spec/fixtures/api/schemas/slack/manifest.json
new file mode 100644
index 00000000000..3be7feec27b
--- /dev/null
+++ b/spec/fixtures/api/schemas/slack/manifest.json
@@ -0,0 +1,1250 @@
+{
+ "type": "object",
+ "title": "App Manifest",
+ "required": [
+ "display_information"
+ ],
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "format": "uri"
+ },
+ "_metadata": {
+ "$ref": "#/definitions/app-manifests.metadata"
+ },
+ "app_directory": {
+ "$ref": "#/definitions/app-manifests.v1.app_directory.schema"
+ },
+ "display_information": {
+ "$ref": "#/definitions/app-manifests.v1.display_information.schema"
+ },
+ "features": {
+ "$ref": "#/definitions/app-manifests.v1.features.schema"
+ },
+ "oauth_config": {
+ "$ref": "#/definitions/app-manifests.v1.oauth_config.schema"
+ },
+ "settings": {
+ "$ref": "#/definitions/app-manifests.v1.settings.schema"
+ },
+ "functions": {
+ "$ref": "#/definitions/app-manifests.v2.hermes.functions"
+ },
+ "workflows": {
+ "$ref": "#/definitions/app-manifests.v2.hermes.workflows"
+ },
+ "datastores": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.datastores"
+ },
+ "outgoing_domains": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.outgoing_domains"
+ },
+ "types": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.types"
+ },
+ "external_auth_providers": {
+ "$ref": "#/definitions/app-manifests.v2.hermes.third_party_auth.providers"
+ }
+ },
+ "description": "Describes core app information and functionality.",
+ "definitions": {
+ "app-manifests.metadata": {
+ "type": "object",
+ "properties": {
+ "schema_version": {
+ "type": "integer"
+ },
+ "min_version": {
+ "type": "integer"
+ },
+ "major_version": {
+ "type": "integer",
+ "description": "An integer that specifies the major version of the manifest schema to target."
+ },
+ "minor_version": {
+ "type": "integer",
+ "description": "An integer that specifies the minor version of the manifest schema to target."
+ }
+ },
+ "description": "A group of settings that describe the manifest",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.app_directory.schema": {
+ "type": "object",
+ "required": [
+ "installation_landing_page",
+ "privacy_policy_url",
+ "support_url",
+ "support_email",
+ "supported_languages",
+ "pricing"
+ ],
+ "properties": {
+ "app_directory_categories": {
+ "type": "array",
+ "maxItems": 3,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ }
+ },
+ "use_direct_install": {
+ "type": "boolean"
+ },
+ "direct_install_url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 255
+ },
+ "installation_landing_page": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 255
+ },
+ "privacy_policy_url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 500
+ },
+ "support_url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 350
+ },
+ "support_email": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
+ "maxLength": 100,
+ "_note": "Regex follows HTML5 spec for an email address, not RFC 5322"
+ },
+ "supported_languages": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "type": "string",
+ "maxLength": 50
+ }
+ },
+ "pricing": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 30
+ }
+ },
+ "description": "Information displayed in the App Directory.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.display_information.schema": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 35,
+ "description": "A string of the name of the app. Maximum length is 35 characters."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 140,
+ "description": "A string with a short description of the app for display to users. Maximum length is 140 characters."
+ },
+ "background_color": {
+ "type": "string",
+ "regex": "^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$",
+ "minLength": 4,
+ "maxLength": 7,
+ "description": "A string containing a hex color value (including the hex sign) that specifies the background color used on hovercards that display information about your app. Can be 3-digit (#000) or 6-digit (#000000) hex values. Once an app has set a background color value, it cannot be removed, only updated."
+ },
+ "long_description": {
+ "type": "string",
+ "minLength": 175,
+ "maxLength": 4000,
+ "description": "A string with a longer version of the description of the app. Maximum length is 4000 characters."
+ }
+ },
+ "description": "A group of settings that describe parts of an app's appearance within Slack. If you're distributing the app via the App Directory, read our listing guidelines to pick the best values for these settings",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.features.app_home": {
+ "type": "object",
+ "properties": {
+ "home_tab_enabled": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not the Home tab is enabled."
+ },
+ "messages_tab_enabled": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not the Messages tab in your App Home is enabled."
+ },
+ "messages_tab_read_only_enabled": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not the users can send messages to your app in the Messages tab of your App Home."
+ }
+ },
+ "description": "A group of settings corresponding to the Features section of the app config pages.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.features.bot_user": {
+ "type": "object",
+ "required": [
+ "display_name"
+ ],
+ "properties": {
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "A string containing the display name of the bot user. Maximum length is 80 characters."
+ },
+ "always_online": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not the bot user will always appear to be online."
+ }
+ },
+ "description": "A subgroup of settings that describe bot user configuration.",
+ "additionalProperties": false
+ },
+ "slack-functions.parameter": {
+ "type": "object",
+ "required": [
+ "type",
+ "name"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "title": "Slack function parameter type",
+ "enum": [
+ "integer",
+ "number",
+ "boolean",
+ "string",
+ "object",
+ "array",
+ "slack#/types/user_context",
+ "slack#/types/user_permission",
+ "slack#/types/user_id",
+ "slack#/types/channel_id",
+ "slack#/types/usergroup_id",
+ "slack#/types/timestamp",
+ "slack#/types/blocks",
+ "slack#/types/credential/oauth2",
+ "slack#/types/date",
+ "slack#/types/interactivity",
+ "slack#/types/rich_text",
+ "slack#/types/form_input",
+ "slack#/types/form_input_object",
+ "slack#/types/message_ts",
+ "slack#/types/message_context"
+ ]
+ },
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "is_required": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+ },
+ "app-manifests.v1.features.functions": {
+ "type": "array",
+ "minItems": 0,
+ "maxItems": 50,
+ "items": {
+ "type": "object",
+ "required": [
+ "callback_id",
+ "title",
+ "description",
+ "input_parameters",
+ "output_parameters"
+ ],
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "callback_id": {
+ "type": "string"
+ },
+ "input_parameters": {
+ "type": "array",
+ "minItems": 0,
+ "maxItems": 20,
+ "items": {
+ "$ref": "#/definitions/slack-functions.parameter"
+ }
+ },
+ "output_parameters": {
+ "type": "array",
+ "minItems": 0,
+ "maxItems": 20,
+ "items": {
+ "$ref": "#/definitions/slack-functions.parameter"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "description": "Make functionality of your app reusable."
+ },
+ "app-manifests.v1.features.shortcuts": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "type",
+ "callback_id",
+ "description"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "A string containing the name of the shortcut."
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "message",
+ "global"
+ ],
+ "description": "A string containing one of message or global. This specifies which type of shortcut is being described."
+ },
+ "callback_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "A string containing the callback_id of this shortcut. Maximum length is 255 characters."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 150,
+ "description": "A string containing a short description of this shortcut. Maximum length is 150 characters."
+ }
+ },
+ "additionalProperties": false
+ },
+ "description": "An array of settings groups that describe shortcuts configuration. A maximum of 5 shortcuts can be included in this array."
+ },
+ "app-manifests.v1.features.slash_commands": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "type": "object",
+ "required": [
+ "command",
+ "description"
+ ],
+ "properties": {
+ "command": {
+ "type": "string",
+ "pattern": "^\\/",
+ "minLength": 2,
+ "maxLength": 32,
+ "description": "A string containing the actual slash command. Maximum length is 32 characters, and should include the leading / character."
+ },
+ "url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 3000,
+ "description": "A string containing the full https URL that acts as the slash command's request URL"
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000,
+ "description": "A string containing a description of the slash command that will be displayed to users. Maximum length is 2000 characters."
+ },
+ "usage_hint": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1000,
+ "description": "A string a short usage hint about the slash command for users. Maximum length is 1000 characters."
+ },
+ "should_escape": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not channels, users, and links typed with the slash command should be escaped.",
+ "default": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "description": "An array of settings groups that describe slash commands configuration. A maximum of 5 slash commands can be included in this array."
+ },
+ "app-manifests.v1.features.workflow_steps": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "callback_id"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "description": "A string containing the name of the workflow step. Maximum length of 50 characters."
+ },
+ "callback_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "description": "A string containing the callback_id of the workflow step. Maximum length of 50 characters."
+ }
+ },
+ "additionalProperties": false
+ },
+ "description": "An array of settings groups that describe workflow steps configuration. A maximum of 10 workflow steps can be included in this array."
+ },
+ "app-manifests.v1.features.unfurl_domains": {
+ "uniqueItems": true,
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 5,
+ "items": {
+ "type": "string",
+ "pattern": "^(?![\\.\\-])([-a-z0-9\\.])+([a-z0-9])$",
+ "maxLength": 255
+ },
+ "description": "An array of strings containing valid unfurl domains to register. A maximum of 5 unfurl domains can be included in this array. Please consult the unfurl docs for a list of domain requirements."
+ },
+ "app-manifests.v1.features.schema": {
+ "type": "object",
+ "properties": {
+ "app_home": {
+ "$ref": "#/definitions/app-manifests.v1.features.app_home"
+ },
+ "bot_user": {
+ "$ref": "#/definitions/app-manifests.v1.features.bot_user"
+ },
+ "functions": {
+ "$ref": "#/definitions/app-manifests.v1.features.functions"
+ },
+ "shortcuts": {
+ "$ref": "#/definitions/app-manifests.v1.features.shortcuts"
+ },
+ "slash_commands": {
+ "$ref": "#/definitions/app-manifests.v1.features.slash_commands"
+ },
+ "workflow_steps": {
+ "$ref": "#/definitions/app-manifests.v1.features.workflow_steps"
+ },
+ "unfurl_domains": {
+ "$ref": "#/definitions/app-manifests.v1.features.unfurl_domains"
+ }
+ },
+ "description": "A group of settings corresponding to the Features section of the app config pages.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.oauth_config.redirect_urls": {
+ "uniqueItems": true,
+ "type": "array",
+ "maxItems": 1000,
+ "items": {
+ "type": "string",
+ "maxLength": 2500,
+ "_note": "Not including a regex bc currently we accept anything like '://asdf'"
+ },
+ "description": "An array of strings containing OAuth redirect URLs. A maximum of 1000 redirect URLs can be included in this array."
+ },
+ "app-manifests.v1.oauth_config.scopes": {
+ "type": "object",
+ "properties": {
+ "user": {
+ "type": "array",
+ "maxItems": 255,
+ "items": {
+ "type": "string",
+ "maxLength": 50
+ },
+ "description": "An array of strings containing user scopes to request upon app installation. A maximum of 255 scopes can included in this array."
+ },
+ "bot": {
+ "type": "array",
+ "maxItems": 255,
+ "items": {
+ "type": "string",
+ "maxLength": 50
+ },
+ "description": "An array of strings containing bot scopes to request upon app installation. A maximum of 255 scopes can included in this array."
+ }
+ },
+ "description": "A subgroup of settings that describe permission scopes configuration.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.oauth_config.schema": {
+ "type": "object",
+ "properties": {
+ "redirect_urls": {
+ "$ref": "#/definitions/app-manifests.v1.oauth_config.redirect_urls"
+ },
+ "scopes": {
+ "$ref": "#/definitions/app-manifests.v1.oauth_config.scopes"
+ },
+ "token_management_enabled": {
+ "type": "boolean"
+ }
+ },
+ "description": "A group of settings describing OAuth configuration for the app.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.settings.allowed_ip_address_ranges": {
+ "uniqueItems": true,
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "type": "string",
+ "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
+ },
+ "description": "An array of strings that contain IP addresses that conform to the Allowed IP Ranges feature"
+ },
+ "app-manifests.v1.settings.event_subscriptions": {
+ "type": "object",
+ "properties": {
+ "request_url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 3500,
+ "description": "A string containing the full https URL that acts as the Events API request URL. If set, you'll need to manually verify the Request URL in the App Manifest section of App Management."
+ },
+ "user_events": {
+ "uniqueItems": true,
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "string",
+ "maxLength": 50
+ },
+ "description": "An array of strings matching the event types you want to the app to subscribe to on behalf of authorized users. A maximum of 100 event types can be used."
+ },
+ "bot_events": {
+ "uniqueItems": true,
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "string",
+ "maxLength": 50
+ },
+ "description": "An array of strings matching the event types you want to the app to subscribe to. A maximum of 100 event types can be used."
+ },
+ "metadata_subscriptions": {
+ "uniqueItems": true,
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 20,
+ "items": {
+ "type": "object",
+ "required": [
+ "app_id",
+ "event_type"
+ ],
+ "properties": {
+ "app_id": {
+ "type": "string"
+ },
+ "event_type": {
+ "type": "string",
+ "maxLength": 50
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "description": "A subgroup of settings that describe Events API configuration for the app.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.settings.incoming_webhooks": {
+ "type": "object",
+ "properties": {
+ "incoming_webhooks_enabled": {
+ "type": "boolean"
+ }
+ },
+ "description": "Incoming Webhooks settings",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.settings.interactivity": {
+ "type": "object",
+ "required": [
+ "is_enabled"
+ ],
+ "properties": {
+ "is_enabled": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not interactivity features are enabled."
+ },
+ "request_url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 3500,
+ "description": "A string containing the full https URL that acts as the interactive Request URL."
+ },
+ "message_menu_options_url": {
+ "type": "string",
+ "pattern": "^https?:\\/\\/",
+ "maxLength": 3500,
+ "description": "A string containing the full https URL that acts as the interactive Options Load URL."
+ }
+ },
+ "description": "A subgroup of settings that describe interactivity configuration for the app.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.settings.siws_links": {
+ "type": "object",
+ "properties": {
+ "initiate_uri": {
+ "type": "string",
+ "pattern": "^https:\\/\\/",
+ "maxLength": 3500
+ }
+ },
+ "additionalProperties": false
+ },
+ "app-manifests.v1.settings.schema": {
+ "type": "object",
+ "properties": {
+ "allowed_ip_address_ranges": {
+ "$ref": "#/definitions/app-manifests.v1.settings.allowed_ip_address_ranges"
+ },
+ "event_subscriptions": {
+ "$ref": "#/definitions/app-manifests.v1.settings.event_subscriptions"
+ },
+ "incoming_webhooks": {
+ "$ref": "#/definitions/app-manifests.v1.settings.incoming_webhooks"
+ },
+ "interactivity": {
+ "$ref": "#/definitions/app-manifests.v1.settings.interactivity"
+ },
+ "org_deploy_enabled": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not org-wide deploy is enabled."
+ },
+ "socket_mode_enabled": {
+ "type": "boolean",
+ "description": "A boolean that specifies whether or not Socket Mode is enabled."
+ },
+ "is_hosted": {
+ "type": "boolean"
+ },
+ "token_rotation_enabled": {
+ "type": "boolean"
+ },
+ "siws_links": {
+ "$ref": "#/definitions/app-manifests.v1.settings.siws_links"
+ },
+ "hermes_app_type": {
+ "type": "string"
+ },
+ "function_runtime": {
+ "type": "string"
+ }
+ },
+ "description": "A group of settings corresponding to the Settings section of the app config pages.",
+ "additionalProperties": false
+ },
+ "app-manifests.v1.hermes.types.type": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "title": "Slack primitive type",
+ "enum": [
+ "integer",
+ "number",
+ "boolean",
+ "string",
+ "object",
+ "array",
+ "slack#/types/user_context",
+ "slack#/types/user_permission",
+ "slack#/types/user_id",
+ "slack#/types/channel_id",
+ "slack#/types/usergroup_id",
+ "slack#/types/timestamp",
+ "slack#/types/blocks",
+ "slack#/types/credential/oauth2",
+ "slack#/types/date",
+ "slack#/types/interactivity",
+ "slack#/types/rich_text",
+ "slack#/types/form_input",
+ "slack#/types/form_input_object",
+ "slack#/types/message_ts",
+ "slack#/types/message_context"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "is_required": {
+ "type": "boolean"
+ },
+ "is_hidden": {
+ "type": "boolean"
+ },
+ "hint": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": true
+ },
+ "app-manifests.v1.hermes.types": {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.types.type"
+ }
+ },
+ "minProperties": 0,
+ "maxProperties": 50,
+ "description": "Declare the types the app provides",
+ "default": {
+ "your_type_name": {
+ "type": "string"
+ }
+ }
+ },
+ "app-manifests.v2.hermes.functions": {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "required": [
+ "title",
+ "description",
+ "input_parameters",
+ "output_parameters"
+ ],
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "input_parameters": {
+ "type": "object",
+ "required": [
+ "properties"
+ ],
+ "properties": {
+ "properties": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.types"
+ },
+ "required": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "output_parameters": {
+ "type": "object",
+ "required": [
+ "properties"
+ ],
+ "properties": {
+ "properties": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.types"
+ },
+ "required": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "minProperties": 0,
+ "maxProperties": 50,
+ "description": "Make functionality of your app reusable.",
+ "default": {
+ "your_callback_id": {
+ "title": "Your Function Title",
+ "description": "Your Function Description",
+ "input_parameters": {
+ },
+ "output_parameters": {
+ }
+ }
+ }
+ },
+ "app-manifests.v2.hermes.step": {
+ "type": "object",
+ "required": [
+ "id",
+ "function_id",
+ "inputs"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "function_id": {
+ "type": "string"
+ },
+ "inputs": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {
+ "type": [
+ "string",
+ "object",
+ "array",
+ "number",
+ "boolean"
+ ]
+ }
+ },
+ "default": {
+ "your_input": "string"
+ },
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "function",
+ "switch",
+ "conditional"
+ ]
+ }
+ },
+ "description": "Declare a workflow step",
+ "additionalProperties": false
+ },
+ "slack-functions.value-template": {
+ "type": "object",
+ "required": [
+ "value"
+ ],
+ "properties": {
+ "value": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "object"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "initial_value": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "object"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "locked": {
+ "type": "boolean",
+ "default": false
+ },
+ "hidden": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "description": "An object describing how to render a value at runtime",
+ "additionalProperties": false
+ },
+ "slack-functions.parameter-value-templates": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {
+ "$ref": "#/definitions/slack-functions.value-template"
+ }
+ },
+ "description": "A mapping of parameter names to template objects",
+ "default": {
+ "your_input": {
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "app-manifests.v2.hermes.workflows": {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "required": [
+ "title",
+ "description",
+ "steps"
+ ],
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "input_parameters": {
+ "type": "object",
+ "required": [
+ "properties"
+ ],
+ "properties": {
+ "properties": {
+ "$ref": "#/definitions/app-manifests.v1.hermes.types"
+ },
+ "required": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "steps": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/app-manifests.v2.hermes.step"
+ }
+ },
+ "suggested_triggers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "inputs"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "inputs": {
+ "$ref": "#/definitions/slack-functions.parameter-value-templates"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "minProperties": 0,
+ "maxProperties": 50,
+ "description": "Declare the workflow functions the app provides.",
+ "default": {
+ "your_workflow_id": {
+ "title": "Your Workflow Title",
+ "description": "Your Workflow Description",
+ "steps": [
+
+ ]
+ }
+ }
+ },
+ "app-manifests.v1.hermes.datastores": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {
+ "type": "object",
+ "required": [
+ "primary_key",
+ "attributes"
+ ],
+ "properties": {
+ "primary_key": {
+ "type": "string",
+ "minLength": 1
+ },
+ "attributes": {
+ "type": "object",
+ "patternProperties": {
+ "^.+$": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1
+ },
+ "items": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "properties": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ "minProperties": 0,
+ "maxProperties": 100,
+ "default": {
+ "your_attribute_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "minProperties": 0,
+ "maxProperties": 10,
+ "description": "Declares the datastores used by the app.",
+ "default": {
+ "your_datastore_id": {
+ "primary_key": "Your Primary Key",
+ "attributes": {
+ "your_attribute_id": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "app-manifests.v1.hermes.outgoing_domains": {
+ "uniqueItems": true,
+ "type": "array",
+ "minItems": 0,
+ "maxItems": 10,
+ "items": {
+ "type": "string",
+ "pattern": "^(?![\\.\\-])([-a-z0-9\\.])+([a-z0-9])$",
+ "maxLength": 50
+ },
+ "description": "Allowed Egress Domains for the Hosted App"
+ },
+ "app-manifests.v2.hermes.third_party_auth.providers.oauth2": {
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "required": [
+ "provider_type",
+ "options"
+ ],
+ "properties": {
+ "provider_type": {
+ "type": "string",
+ "enum": [
+ "CUSTOM",
+ "SLACK_PROVIDED"
+ ]
+ },
+ "options": {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "client_id",
+ "scope"
+ ],
+ "properties": {
+ "client_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "scope": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minItems": 0
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "required": [
+ "client_id",
+ "provider_name",
+ "authorization_url",
+ "token_url",
+ "scope",
+ "identity_config"
+ ],
+ "properties": {
+ "client_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "provider_name": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 255
+ },
+ "authorization_url": {
+ "type": "string",
+ "pattern": "^https:\\/\\/",
+ "minLength": 5,
+ "maxLength": 255
+ },
+ "token_url": {
+ "type": "string",
+ "pattern": "^https:\\/\\/",
+ "minLength": 5,
+ "maxLength": 255
+ },
+ "scope": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^(\\x21|[\\x23-\\x5B]|[\\x5D-\\x7E]){1,}$",
+ "minItems": 0
+ }
+ },
+ "authorization_url_extras": {
+ "type": "object"
+ },
+ "identity_config": {
+ "type": "object",
+ "required": [
+ "url",
+ "account_identifier"
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ "pattern": "^https:\\/\\/",
+ "minLength": 5,
+ "maxLength": 255
+ },
+ "account_identifier": {
+ "type": "string",
+ "pattern": "^\\$\\.(.)+",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "headers": {
+ "type": "object"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "description": "Declares the oauth configurations used by the app.",
+ "default": {
+ "your_oauth2_id": {
+ "provider_type": "CUSTOM",
+ "options": {
+ "client_id": "Your Client ID",
+ "scope": [
+
+ ]
+ }
+ }
+ }
+ },
+ "app-manifests.v2.hermes.third_party_auth.providers": {
+ "type": "object",
+ "properties": {
+ "oauth2": {
+ "$ref": "#/definitions/app-manifests.v2.hermes.third_party_auth.providers.oauth2"
+ }
+ },
+ "description": "Declares the oauth configurations used by the app.",
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
diff --git a/spec/fixtures/csv_missing_milestones.csv b/spec/fixtures/csv_missing_milestones.csv
new file mode 100644
index 00000000000..ec2b20df018
--- /dev/null
+++ b/spec/fixtures/csv_missing_milestones.csv
@@ -0,0 +1,5 @@
+title,description,milestone
+"Issue with missing milestone","",15.10,
+"Issue without milestone","",,
+"Issue with milestone","",10.1,
+"Issue with duplicate milestone","",15.10,
diff --git a/spec/fixtures/grafana/expected_grafana_embed.json b/spec/fixtures/grafana/expected_grafana_embed.json
index 72fb5477b9e..0cee0385886 100644
--- a/spec/fixtures/grafana/expected_grafana_embed.json
+++ b/spec/fixtures/grafana/expected_grafana_embed.json
@@ -10,14 +10,12 @@
{
"id": "In_0",
"query_range": "sum( rate(redis_net_input_bytes_total{instance=~\"localhost:9121\"}[1m]))",
- "label": "In",
- "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_input_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+ "label": "In"
},
{
"id": "Out_1",
"query_range": "sum( rate(redis_net_output_bytes_total{instance=~\"localhost:9121\"}[1m]))",
- "label": "Out",
- "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_output_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+ "label": "Out"
}
]
}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
index 3955107865d..8b6dfaf72d7 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
@@ -3,8 +3,8 @@
{"id":38,"title":"Quasi adipisci non cupiditate dolorem quo qui earum sed.","author_id":6,"project_id":5,"created_at":"2016-06-14T15:02:08.154Z","updated_at":"2016-06-14T15:02:48.614Z","position":0,"branch_name":null,"description":"Ea recusandae neque autem tempora.","state":"closed","iid":8,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"label_links":[{"id":99,"label_id":2,"target_id":38,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"notes":[{"id":367,"note":"Accusantium fugiat et eaque quisquam esse corporis.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.235Z","updated_at":"2016-06-14T15:02:48.235Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":368,"note":"Ea labore eum nam qui laboriosam.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.261Z","updated_at":"2016-06-14T15:02:48.261Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":369,"note":"Accusantium quis sed molestiae et.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.294Z","updated_at":"2016-06-14T15:02:48.294Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":370,"note":"Corporis numquam a voluptatem pariatur asperiores dolorem delectus autem.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.523Z","updated_at":"2016-06-14T15:02:48.523Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":371,"note":"Ea accusantium maxime voluptas rerum.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.546Z","updated_at":"2016-06-14T15:02:48.546Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":372,"note":"Pariatur iusto et et excepturi similique ipsam eum.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.569Z","updated_at":"2016-06-14T15:02:48.569Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":373,"note":"Aliquam et culpa officia iste eius.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.591Z","updated_at":"2016-06-14T15:02:48.591Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":374,"note":"Ab id velit id unde laborum.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.613Z","updated_at":"2016-06-14T15:02:48.613Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
{"id":37,"title":"Cupiditate quo aut ducimus minima molestiae vero numquam possimus.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:08.051Z","updated_at":"2016-06-14T15:02:48.854Z","position":0,"branch_name":null,"description":"Maiores architecto quos in dolorem.","state":"opened","iid":7,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":375,"note":"Quasi fugit qui sed eligendi aut quia.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.647Z","updated_at":"2016-06-14T15:02:48.647Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":376,"note":"Esse nesciunt voluptatem ex vero est consequatur.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.674Z","updated_at":"2016-06-14T15:02:48.674Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":377,"note":"Similique qui quas non aut et velit sequi in.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.696Z","updated_at":"2016-06-14T15:02:48.696Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":378,"note":"Eveniet ut cupiditate repellendus numquam in esse eius.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.720Z","updated_at":"2016-06-14T15:02:48.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":379,"note":"Velit est dolorem adipisci rerum sed iure.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.755Z","updated_at":"2016-06-14T15:02:48.755Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":380,"note":"Voluptatem ullam ab ut illo ut quo.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.793Z","updated_at":"2016-06-14T15:02:48.793Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":381,"note":"Voluptatem impedit beatae quasi ipsa earum consectetur.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.823Z","updated_at":"2016-06-14T15:02:48.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":382,"note":"Nihil officiis eaque incidunt sunt voluptatum excepturi.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.852Z","updated_at":"2016-06-14T15:02:48.852Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
{"id":36,"title":"Necessitatibus dolor est enim quia rem suscipit quidem voluptas ullam.","author_id":16,"project_id":5,"created_at":"2016-06-14T15:02:07.958Z","updated_at":"2016-06-14T15:02:49.044Z","position":0,"branch_name":null,"description":"Ut aut ut et tenetur velit aut id modi.","state":"opened","iid":6,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":383,"note":"Excepturi deleniti sunt rerum nesciunt vero fugiat possimus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.885Z","updated_at":"2016-06-14T15:02:48.885Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":384,"note":"Et est nemo sed nam sed.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.910Z","updated_at":"2016-06-14T15:02:48.910Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":385,"note":"Animi mollitia nulla facere amet aut quaerat.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.934Z","updated_at":"2016-06-14T15:02:48.934Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":386,"note":"Excepturi id voluptas ut odio officiis omnis.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.955Z","updated_at":"2016-06-14T15:02:48.955Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":387,"note":"Molestiae labore officiis magni et eligendi quasi maxime.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.978Z","updated_at":"2016-06-14T15:02:48.978Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":388,"note":"Officia tenetur praesentium rem nam non.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.001Z","updated_at":"2016-06-14T15:02:49.001Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":389,"note":"Et et et molestiae reprehenderit.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.022Z","updated_at":"2016-06-14T15:02:49.022Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":390,"note":"Aperiam in consequatur est sunt cum quia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.043Z","updated_at":"2016-06-14T15:02:49.043Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
-{"id":35,"title":"Repellat praesentium deserunt maxime incidunt harum porro qui.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:07.832Z","updated_at":"2016-06-14T15:02:49.226Z","position":0,"branch_name":null,"description":"Dicta nisi nihil non ipsa velit.","state":"closed","iid":5,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":391,"note":"Qui magnam et assumenda quod id dicta necessitatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.075Z","updated_at":"2016-06-14T15:02:49.075Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":392,"note":"Consectetur deserunt possimus dolor est odio.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.095Z","updated_at":"2016-06-14T15:02:49.095Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":393,"note":"Labore nisi quo cumque voluptas consequatur aut qui.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.117Z","updated_at":"2016-06-14T15:02:49.117Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":394,"note":"Et totam facilis voluptas et enim.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.138Z","updated_at":"2016-06-14T15:02:49.138Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":395,"note":"Ratione sint pariatur sed omnis eligendi quo libero exercitationem.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.160Z","updated_at":"2016-06-14T15:02:49.160Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":396,"note":"Iure hic autem id voluptatem.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.182Z","updated_at":"2016-06-14T15:02:49.182Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":397,"note":"Excepturi eum laboriosam delectus repellendus odio nisi et voluptatem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.205Z","updated_at":"2016-06-14T15:02:49.205Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":398,"note":"Ut quis ex soluta consequatur et blanditiis.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.225Z","updated_at":"2016-06-14T15:02:49.225Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":35,"title":"task by both attributes","work_item_type":{"base_type":"task"},"issue_type":"incident","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:07.832Z","updated_at":"2016-06-14T15:02:49.226Z","position":0,"branch_name":null,"description":"Dicta nisi nihil non ipsa velit.","state":"closed","iid":5,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":391,"note":"Qui magnam et assumenda quod id dicta necessitatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.075Z","updated_at":"2016-06-14T15:02:49.075Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":392,"note":"Consectetur deserunt possimus dolor est odio.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.095Z","updated_at":"2016-06-14T15:02:49.095Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":393,"note":"Labore nisi quo cumque voluptas consequatur aut qui.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.117Z","updated_at":"2016-06-14T15:02:49.117Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":394,"note":"Et totam facilis voluptas et enim.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.138Z","updated_at":"2016-06-14T15:02:49.138Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":395,"note":"Ratione sint pariatur sed omnis eligendi quo libero exercitationem.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.160Z","updated_at":"2016-06-14T15:02:49.160Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":396,"note":"Iure hic autem id voluptatem.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.182Z","updated_at":"2016-06-14T15:02:49.182Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":397,"note":"Excepturi eum laboriosam delectus repellendus odio nisi et voluptatem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.205Z","updated_at":"2016-06-14T15:02:49.205Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":398,"note":"Ut quis ex soluta consequatur et blanditiis.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.225Z","updated_at":"2016-06-14T15:02:49.225Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
{"id":34,"title":"Ullam expedita deserunt libero consequatur quia dolor harum perferendis facere quidem.","author_id":1,"project_id":5,"created_at":"2016-06-14T15:02:07.717Z","updated_at":"2016-06-14T15:02:49.416Z","position":0,"branch_name":null,"description":"Ut et explicabo vel voluptatem consequuntur ut sed.","state":"closed","iid":4,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":399,"note":"Dolor iste tempora tenetur non vitae maiores voluptatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.256Z","updated_at":"2016-06-14T15:02:49.256Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":400,"note":"Aut sit quidem qui adipisci maxime excepturi iusto.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.284Z","updated_at":"2016-06-14T15:02:49.284Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":401,"note":"Et a necessitatibus autem quidem animi sunt voluptatum rerum.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.305Z","updated_at":"2016-06-14T15:02:49.305Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":402,"note":"Esse laboriosam quo voluptatem quis molestiae.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.328Z","updated_at":"2016-06-14T15:02:49.328Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":403,"note":"Nemo magnam distinctio est ut voluptate ea.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.350Z","updated_at":"2016-06-14T15:02:49.350Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":404,"note":"Omnis sed rerum neque rerum quae quam nulla officiis.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.372Z","updated_at":"2016-06-14T15:02:49.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":405,"note":"Quo soluta dolorem vitae ad consequatur qui aut dicta.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.394Z","updated_at":"2016-06-14T15:02:49.394Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":406,"note":"Magni minus est aut aut totam ut.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.414Z","updated_at":"2016-06-14T15:02:49.414Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
-{"id":33,"title":"Numquam accusamus eos iste exercitationem magni non inventore.","author_id":26,"project_id":5,"created_at":"2016-06-14T15:02:07.611Z","updated_at":"2016-06-14T15:02:49.661Z","position":0,"branch_name":null,"description":"Non asperiores velit accusantium voluptate.","state":"closed","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":407,"note":"Quod ea et possimus architecto.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.450Z","updated_at":"2016-06-14T15:02:49.450Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":408,"note":"Reiciendis est et unde perferendis dicta ut praesentium quasi.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.503Z","updated_at":"2016-06-14T15:02:49.503Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":409,"note":"Magni quia odio blanditiis pariatur voluptas.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.527Z","updated_at":"2016-06-14T15:02:49.527Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":410,"note":"Enim quam ut et et et.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.551Z","updated_at":"2016-06-14T15:02:49.551Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":411,"note":"Fugit voluptatem ratione maxime expedita.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.578Z","updated_at":"2016-06-14T15:02:49.578Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":412,"note":"Voluptatem enim aut ipsa et et ducimus.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.604Z","updated_at":"2016-06-14T15:02:49.604Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":413,"note":"Quia repellat fugiat consectetur quidem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.631Z","updated_at":"2016-06-14T15:02:49.631Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":414,"note":"Corporis ipsum et ea necessitatibus quod assumenda repudiandae quam.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.659Z","updated_at":"2016-06-14T15:02:49.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
-{"id":32,"title":"Necessitatibus magnam qui at velit consequatur perspiciatis.","author_id":15,"project_id":5,"created_at":"2016-06-14T15:02:07.431Z","updated_at":"2016-06-14T15:02:49.884Z","position":0,"branch_name":null,"description":"Molestiae corporis magnam et fugit aliquid nulla quia.","state":"closed","iid":2,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":415,"note":"Nemo consequatur sed blanditiis qui id iure dolores.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.694Z","updated_at":"2016-06-14T15:02:49.694Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":416,"note":"Voluptas ab accusantium dicta in.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.718Z","updated_at":"2016-06-14T15:02:49.718Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":417,"note":"Esse odit qui a et eum ducimus.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.741Z","updated_at":"2016-06-14T15:02:49.741Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":418,"note":"Sequi dolor doloribus ratione placeat repellendus.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.767Z","updated_at":"2016-06-14T15:02:49.767Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":419,"note":"Quae aspernatur rem est similique.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.796Z","updated_at":"2016-06-14T15:02:49.796Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":420,"note":"Voluptate omnis et id rerum non nesciunt laudantium assumenda.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.825Z","updated_at":"2016-06-14T15:02:49.825Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":421,"note":"Quia enim ab et eligendi.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.853Z","updated_at":"2016-06-14T15:02:49.853Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":422,"note":"In fugiat rerum voluptas quas officia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.881Z","updated_at":"2016-06-14T15:02:49.881Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":33,"title":"task by issue_type","issue_type":"task","author_id":26,"project_id":5,"created_at":"2016-06-14T15:02:07.611Z","updated_at":"2016-06-14T15:02:49.661Z","position":0,"branch_name":null,"description":"Non asperiores velit accusantium voluptate.","state":"closed","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":407,"note":"Quod ea et possimus architecto.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.450Z","updated_at":"2016-06-14T15:02:49.450Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":408,"note":"Reiciendis est et unde perferendis dicta ut praesentium quasi.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.503Z","updated_at":"2016-06-14T15:02:49.503Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":409,"note":"Magni quia odio blanditiis pariatur voluptas.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.527Z","updated_at":"2016-06-14T15:02:49.527Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":410,"note":"Enim quam ut et et et.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.551Z","updated_at":"2016-06-14T15:02:49.551Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":411,"note":"Fugit voluptatem ratione maxime expedita.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.578Z","updated_at":"2016-06-14T15:02:49.578Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":412,"note":"Voluptatem enim aut ipsa et et ducimus.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.604Z","updated_at":"2016-06-14T15:02:49.604Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":413,"note":"Quia repellat fugiat consectetur quidem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.631Z","updated_at":"2016-06-14T15:02:49.631Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":414,"note":"Corporis ipsum et ea necessitatibus quod assumenda repudiandae quam.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.659Z","updated_at":"2016-06-14T15:02:49.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":32,"title":"incident by work_item_type","work_item_type":{"base_type":"incident"},"author_id":15,"project_id":5,"created_at":"2016-06-14T15:02:07.431Z","updated_at":"2016-06-14T15:02:49.884Z","position":0,"branch_name":null,"description":"Molestiae corporis magnam et fugit aliquid nulla quia.","state":"closed","iid":2,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":415,"note":"Nemo consequatur sed blanditiis qui id iure dolores.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.694Z","updated_at":"2016-06-14T15:02:49.694Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":416,"note":"Voluptas ab accusantium dicta in.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.718Z","updated_at":"2016-06-14T15:02:49.718Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":417,"note":"Esse odit qui a et eum ducimus.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.741Z","updated_at":"2016-06-14T15:02:49.741Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":418,"note":"Sequi dolor doloribus ratione placeat repellendus.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.767Z","updated_at":"2016-06-14T15:02:49.767Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":419,"note":"Quae aspernatur rem est similique.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.796Z","updated_at":"2016-06-14T15:02:49.796Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":420,"note":"Voluptate omnis et id rerum non nesciunt laudantium assumenda.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.825Z","updated_at":"2016-06-14T15:02:49.825Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":421,"note":"Quia enim ab et eligendi.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.853Z","updated_at":"2016-06-14T15:02:49.853Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":422,"note":"In fugiat rerum voluptas quas officia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.881Z","updated_at":"2016-06-14T15:02:49.881Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
{"id":31,"title":"issue_with_timelogs","author_id":16,"project_id":5,"created_at":"2016-06-14T15:02:07.280Z","updated_at":"2016-06-14T15:02:50.134Z","position":0,"branch_name":null,"description":"Quod ad architecto qui est sed quia.","state":"closed","iid":1,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"timelogs":[{"id":1,"time_spent":72000,"user_id":1,"created_at":"2019-12-27T09:15:22.302Z","updated_at":"2019-12-27T09:15:22.302Z","spent_at":"2019-12-27T00:00:00.000Z"}],"notes":[{"id":423,"note":"A mollitia qui iste consequatur eaque iure omnis sunt.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.933Z","updated_at":"2016-06-14T15:02:49.933Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":424,"note":"Eveniet est et blanditiis sequi alias.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.965Z","updated_at":"2016-06-14T15:02:49.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":425,"note":"Commodi tempore voluptas doloremque est.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.996Z","updated_at":"2016-06-14T15:02:49.996Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":426,"note":"Quo libero impedit odio debitis rerum aspernatur.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:50.024Z","updated_at":"2016-06-14T15:02:50.024Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":427,"note":"Dolorem voluptatem qui labore deserunt.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:50.049Z","updated_at":"2016-06-14T15:02:50.049Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":428,"note":"Est blanditiis laboriosam enim ipsam.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:50.077Z","updated_at":"2016-06-14T15:02:50.077Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":429,"note":"Et in voluptatem animi dolorem eos.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:50.107Z","updated_at":"2016-06-14T15:02:50.107Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":430,"note":"Unde culpa voluptate qui sint quos.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:50.132Z","updated_at":"2016-06-14T15:02:50.132Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
index 8ee207b7ebf..79132c0d039 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -2,23 +2,57 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
- "label",
- "prometheus_endpoint_path"
+ "label"
],
"oneOf": [
- { "required": ["query"] },
- { "required": ["query_range"] }
+ {
+ "required": [
+ "query"
+ ]
+ },
+ {
+ "required": [
+ "query_range"
+ ]
+ }
],
"properties": {
- "id": { "type": "string" },
- "query_range": { "type": ["string", "number"] },
- "query": { "type": ["string", "number"] },
- "unit": { "type": "string" },
- "label": { "type": "string" },
- "track": { "type": "string" },
- "prometheus_endpoint_path": { "type": "string" },
- "metric_id": { "type": "number" },
- "edit_path": { "type": ["string", "null"] }
+ "id": {
+ "type": "string"
+ },
+ "query_range": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "query": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "unit": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "track": {
+ "type": "string"
+ },
+ "prometheus_endpoint_path": {
+ "type": "string"
+ },
+ "metric_id": {
+ "type": "number"
+ },
+ "edit_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
"additionalProperties": false
}
diff --git a/spec/frontend/__helpers__/set_vue_error_handler.js b/spec/frontend/__helpers__/set_vue_error_handler.js
new file mode 100644
index 00000000000..d254630d1e4
--- /dev/null
+++ b/spec/frontend/__helpers__/set_vue_error_handler.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+
+const modifiedInstances = [];
+
+export function setVueErrorHandler({ instance, handler }) {
+ if (Vue.version.startsWith('2')) {
+ // only global handlers are supported
+ const { config } = Vue;
+ config.errorHandler = handler;
+ return;
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ instance.$.appContext.config.errorHandler = handler;
+ modifiedInstances.push(instance);
+}
+
+export function resetVueErrorHandler() {
+ if (Vue.version.startsWith('2')) {
+ const { config } = Vue;
+ config.errorHandler = null;
+ return;
+ }
+
+ modifiedInstances.forEach((instance) => {
+ // eslint-disable-next-line no-param-reassign
+ instance.$.appContext.config.errorHandler = null;
+ });
+ modifiedInstances.length = 0;
+}
diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js
index 6e7dee6a2cc..59cc8e25414 100644
--- a/spec/frontend/access_tokens/components/tokens_app_spec.js
+++ b/spec/frontend/access_tokens/components/tokens_app_spec.js
@@ -43,8 +43,8 @@ describe('TokensApp', () => {
}) => {
const container = extendedWrapper(wrapper.findByTestId(testId));
- expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true);
- expect(container.findByText(expectedDescription).exists()).toBe(true);
+ expect(container.findByText(expectedLabel).exists()).toBe(true);
+ expect(container.findByText(expectedDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText('reset this token').attributes()).toMatchObject({
'data-confirm': expectedResetConfirmMessage,
diff --git a/spec/frontend/actioncable_connection_monitor_spec.js b/spec/frontend/actioncable_connection_monitor_spec.js
deleted file mode 100644
index c68eb53acde..00000000000
--- a/spec/frontend/actioncable_connection_monitor_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import ConnectionMonitor from '~/actioncable_connection_monitor';
-
-describe('ConnectionMonitor', () => {
- let monitor;
-
- beforeEach(() => {
- monitor = new ConnectionMonitor({});
- });
-
- describe('#getPollInterval', () => {
- beforeEach(() => {
- Math.originalRandom = Math.random;
- });
- afterEach(() => {
- Math.random = Math.originalRandom;
- });
-
- const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor;
- const backoffFactor = 1 + reconnectionBackoffRate;
- const ms = 1000;
-
- it('uses exponential backoff', () => {
- Math.random = () => 0;
-
- monitor.reconnectAttempts = 0;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
-
- monitor.reconnectAttempts = 1;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms);
-
- monitor.reconnectAttempts = 2;
- expect(monitor.getPollInterval()).toEqual(
- staleThreshold * backoffFactor * backoffFactor * ms,
- );
- });
-
- it('caps exponential backoff after some number of reconnection attempts', () => {
- Math.random = () => 0;
- monitor.reconnectAttempts = 42;
- const cappedPollInterval = monitor.getPollInterval();
-
- monitor.reconnectAttempts = 9001;
- expect(monitor.getPollInterval()).toEqual(cappedPollInterval);
- });
-
- it('uses 100% jitter when 0 reconnection attempts', () => {
- Math.random = () => 0;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
-
- Math.random = () => 0.5;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms);
- });
-
- it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => {
- monitor.reconnectAttempts = 1;
-
- Math.random = () => 0.25;
- expect(monitor.getPollInterval()).toEqual(
- staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms,
- );
-
- Math.random = () => 0.5;
- expect(monitor.getPollInterval()).toEqual(
- staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms,
- );
- });
-
- it('applies jitter after capped exponential backoff', () => {
- monitor.reconnectAttempts = 9001;
-
- Math.random = () => 0;
- const withoutJitter = monitor.getPollInterval();
- Math.random = () => 0.5;
- const withJitter = monitor.getPollInterval();
-
- expect(withJitter).toBeGreaterThan(withoutJitter);
- });
- });
-});
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 27fe010c354..fa051f7a43a 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
@@ -45,14 +45,13 @@ describe('AddContextCommitsModal', () => {
...props,
},
});
- return wrapper;
};
const findModal = () => wrapper.findComponent(GlModal);
const findSearch = () => wrapper.findComponent(GlFilteredSearch);
beforeEach(() => {
- wrapper = createWrapper();
+ createWrapper();
});
it('renders modal with 2 tabs', () => {
@@ -98,7 +97,7 @@ describe('AddContextCommitsModal', () => {
});
it('enabled ok button when atleast one row is selected', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
await nextTick();
expect(findModal().attributes('ok-disabled')).toBe(undefined);
});
@@ -106,14 +105,14 @@ describe('AddContextCommitsModal', () => {
describe('when in second tab, renders a modal with', () => {
beforeEach(() => {
- wrapper.vm.$store.state.tabIndex = 1;
+ store.state.tabIndex = 1;
});
it('a disabled ok button when no row is selected', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('an enabled ok button when atleast one row is selected', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
await nextTick();
expect(findModal().attributes('ok-disabled')).toBe(undefined);
});
@@ -126,7 +125,7 @@ describe('AddContextCommitsModal', () => {
describe('has an ok button when clicked calls action', () => {
it('"createContextCommits" when only new commits to be added', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
await nextTick();
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
@@ -135,14 +134,14 @@ describe('AddContextCommitsModal', () => {
});
});
it('"removeContextCommits" when only added commits are to be removed', async () => {
- wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
+ store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
await nextTick();
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
});
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
- wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
await nextTick();
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js
new file mode 100644
index 00000000000..456df3b1857
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js
@@ -0,0 +1,43 @@
+import { GlLabel } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AbuseCategory from '~/admin/abuse_reports/components/abuse_category.vue';
+import { ABUSE_CATEGORIES } from '~/admin/abuse_reports/constants';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseCategory', () => {
+ let wrapper;
+
+ const mockAbuseReport = mockAbuseReports[0];
+ const category = ABUSE_CATEGORIES[mockAbuseReport.category];
+
+ const findLabel = () => wrapper.findComponent(GlLabel);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(AbuseCategory, {
+ propsData: {
+ category: mockAbuseReport.category,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a label', () => {
+ expect(findLabel().exists()).toBe(true);
+ });
+
+ it('renders the label with the right background color for the category', () => {
+ expect(findLabel().props()).toMatchObject({
+ backgroundColor: category.backgroundColor,
+ title: category.title,
+ target: null,
+ });
+ });
+
+ it('renders the label with the right text color for the category', () => {
+ expect(findLabel().attributes('class')).toBe(`gl-text-${category.color}`);
+ });
+});
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
index f3cced81478..03bf510f3ad 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -1,6 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import AbuseCategory from '~/admin/abuse_reports/components/abuse_category.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { SORT_UPDATED_AT } from '~/admin/abuse_reports/constants';
@@ -11,7 +12,8 @@ describe('AbuseReportRow', () => {
const mockAbuseReport = mockAbuseReports[0];
const findListItem = () => wrapper.findComponent(ListItem);
- const findTitle = () => wrapper.findByTestId('title');
+ const findAbuseCategory = () => wrapper.findComponent(AbuseCategory);
+ const findAbuseReportTitle = () => wrapper.findByTestId('abuse-report-title');
const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date');
const createComponent = (props = {}) => {
@@ -35,13 +37,13 @@ describe('AbuseReportRow', () => {
const { reporter, reportedUser, category, reportPath } = mockAbuseReport;
it('displays correctly formatted title', () => {
- expect(findTitle().text()).toMatchInterpolatedText(
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
`${reportedUser.name} reported for ${category} by ${reporter.name}`,
);
});
it('links to the details page', () => {
- expect(findTitle().attributes('href')).toEqual(reportPath);
+ expect(findAbuseReportTitle().attributes('href')).toEqual(reportPath);
});
describe('when the reportedUser is missing', () => {
@@ -50,7 +52,7 @@ describe('AbuseReportRow', () => {
});
it('displays correctly formatted title', () => {
- expect(findTitle().text()).toMatchInterpolatedText(
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
`Deleted user reported for ${category} by ${reporter.name}`,
);
});
@@ -62,7 +64,7 @@ describe('AbuseReportRow', () => {
});
it('displays correctly formatted title', () => {
- expect(findTitle().text()).toMatchInterpolatedText(
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
`${reportedUser.name} reported for ${category} by Deleted user`,
);
});
@@ -88,4 +90,8 @@ describe('AbuseReportRow', () => {
});
});
});
+
+ it('renders abuse category', () => {
+ expect(findAbuseCategory().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js
index 315c38a2bbc..e0282b8c149 100644
--- a/spec/frontend/admin/applications/components/delete_application_spec.js
+++ b/spec/frontend/admin/applications/components/delete_application_spec.js
@@ -1,6 +1,7 @@
import { GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { stubComponent } from 'helpers/stub_component';
import DeleteApplication from '~/admin/applications/components/delete_application.vue';
const path = 'application/path/1';
@@ -14,6 +15,11 @@ describe('DeleteApplication', () => {
const createComponent = () => {
wrapper = shallowMount(DeleteApplication, {
stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
GlSprintf,
},
});
@@ -36,7 +42,6 @@ describe('DeleteApplication', () => {
describe('the modal component', () => {
beforeEach(() => {
- wrapper.vm.$refs.deleteModal.show = jest.fn();
document.querySelector('.js-application-delete-button').click();
});
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 dca77e67cac..b937a58a742 100644
--- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -5,7 +5,12 @@ 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';
-import { TYPE_BANNER, TYPE_NOTIFICATION, THEMES } from '~/admin/broadcast_messages/constants';
+import {
+ TYPE_BANNER,
+ TYPE_NOTIFICATION,
+ THEMES,
+ TARGET_OPTIONS,
+} from '~/admin/broadcast_messages/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
@@ -37,6 +42,8 @@ describe('MessageForm', () => {
const findCancelButton = () => wrapper.findComponent('[data-testid=cancel-button]');
const findForm = () => wrapper.findComponent(GlForm);
const findShowInCli = () => wrapper.findComponent('[data-testid=show-in-cli-checkbox]');
+ const findTargetSelect = () => wrapper.findComponent('[data-testid=target-select]');
+ const findTargetPath = () => wrapper.findComponent('[data-testid=target-path-input]');
function createComponent({ broadcastMessage = {} } = {}) {
wrapper = mount(MessageForm, {
@@ -112,10 +119,38 @@ describe('MessageForm', () => {
});
});
- describe('target roles checkboxes', () => {
- it('renders target roles', () => {
+ describe('target select', () => {
+ it('renders the first option and hide target path and target roles when creating message', () => {
createComponent();
- expect(findTargetRoles().exists()).toBe(true);
+ expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[0].value);
+ expect(findTargetRoles().isVisible()).toBe(false);
+ expect(findTargetPath().isVisible()).toBe(false);
+ });
+
+ it('triggers displaying target path and target roles when selecting different options', async () => {
+ createComponent();
+ const options = findTargetSelect().findAll('option');
+ await options.at(1).setSelected();
+ expect(findTargetPath().isVisible()).toBe(true);
+ expect(findTargetRoles().isVisible()).toBe(false);
+
+ await options.at(2).setSelected();
+ expect(findTargetPath().isVisible()).toBe(true);
+ expect(findTargetRoles().isVisible()).toBe(true);
+ });
+
+ it('renders the second option and hide target roles when editing message with path specified', () => {
+ createComponent({ broadcastMessage: { targetPath: '/welcome' } });
+ expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[1].value);
+ expect(findTargetRoles().isVisible()).toBe(false);
+ expect(findTargetPath().isVisible()).toBe(true);
+ });
+
+ it('renders the third option when editing message with path and roles specified', () => {
+ createComponent({ broadcastMessage: { targetPath: '/welcome', targetAccessLevels: [20] } });
+ expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[2].value);
+ expect(findTargetRoles().isVisible()).toBe(true);
+ expect(findTargetPath().isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js
index c069203d046..705066c3ef0 100644
--- a/spec/frontend/admin/topics/components/remove_avatar_spec.js
+++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js
@@ -73,7 +73,7 @@ describe('RemoveAvatar', () => {
let formSubmitSpy;
beforeEach(() => {
- formSubmitSpy = jest.spyOn(wrapper.vm.$refs.deleteForm, 'submit');
+ formSubmitSpy = jest.spyOn(findForm().element, 'submit');
findModal().vm.$emit('primary');
});
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index 113a0e3d404..5b7e6365606 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -58,10 +58,6 @@ describe('TopicSelect', () => {
});
}
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('mounts', () => {
createComponent();
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 afd88e1a6ac..9980843defb 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -186,7 +186,7 @@ describe('AlertManagementTable', () => {
expect(findSeverityFields().at(0).text()).toBe('Critical');
});
- it('renders Unassigned when no assignee(s) present', () => {
+ it('renders Unassigned when no assignees present', () => {
mountComponent({
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 4a0c7f65493..e6b38a1e824 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -68,8 +68,11 @@ describe('AlertsSettingsForm', () => {
await options.at(index).setSelected();
};
- const enableIntegration = (index, value) => {
- findFormFields().at(index).setValue(value);
+ const enableIntegration = (index, value = '') => {
+ if (value !== '') {
+ findFormFields().at(index).setValue(value);
+ }
+
findFormToggle().vm.$emit('change', true);
};
@@ -100,7 +103,8 @@ describe('AlertsSettingsForm', () => {
it('hides the name input when the selected value is prometheus', async () => {
createComponent();
await selectOptionAtIndex(2);
- expect(findFormFields().at(0).attributes('id')).not.toBe('name-integration');
+
+ expect(findFormFields()).toHaveLength(0);
});
it('verify pricing link url', () => {
@@ -203,8 +207,8 @@ describe('AlertsSettingsForm', () => {
it('create', async () => {
createComponent();
await selectOptionAtIndex(2);
- const apiUrl = 'https://test.com';
- enableIntegration(0, apiUrl);
+ enableIntegration(0);
+
const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true);
expect(submitBtn.text()).toBe('Save integration');
@@ -213,14 +217,14 @@ describe('AlertsSettingsForm', () => {
expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({
type: typeSet.prometheus,
- variables: { apiUrl, active: true },
+ variables: { active: true },
});
});
it('update', () => {
createComponent({
data: {
- integrationForm: { id: '1', apiUrl: 'https://test-pre.com', type: typeSet.prometheus },
+ integrationForm: { id: '1', type: typeSet.prometheus },
currentIntegration: { id: '1' },
},
props: {
@@ -228,8 +232,7 @@ describe('AlertsSettingsForm', () => {
},
});
- const apiUrl = 'https://test-post.com';
- enableIntegration(0, apiUrl);
+ enableIntegration(0);
const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true);
@@ -239,7 +242,7 @@ describe('AlertsSettingsForm', () => {
expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({
type: typeSet.prometheus,
- variables: { apiUrl, active: true },
+ variables: { active: true },
});
});
});
@@ -442,16 +445,8 @@ describe('AlertsSettingsForm', () => {
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
- it('should not be able to submit when Prometheus integration form is invalid', async () => {
- await selectOptionAtIndex(2);
- await findFormFields().at(0).vm.$emit('input', '');
-
- expect(findSubmitButton().attributes('disabled')).toBeDefined();
- });
-
it('should be able to submit when Prometheus integration form is valid', async () => {
await selectOptionAtIndex(2);
- await findFormFields().at(0).vm.$emit('input', 'http://valid.url');
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
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 4e0b546b3d2..802da47d6cd 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -57,7 +57,6 @@ describe('ProjectsDropdownFilter component', () => {
});
};
- const findClearAllButton = () => wrapper.findByTestId('listbox-reset-button');
const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate);
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -143,10 +142,6 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
-
- it('does not render the clear all button', () => {
- expect(findClearAllButton().exists()).toBe(false);
- });
});
describe('with a selected project', () => {
@@ -169,12 +164,6 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe(projects[0].name);
});
- it('renders the clear all button', async () => {
- await selectDropdownItemAtIndex([0], false);
-
- expect(findClearAllButton().exists()).toBe(true);
- });
-
it('clears all selected items when the clear all button is clicked', async () => {
createComponent({
mountFn: mountExtended,
@@ -186,7 +175,7 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe('2 projects selected');
- await findClearAllButton().vm.$emit('click');
+ await findDropdown().vm.$emit('reset');
expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
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 20836d7cc70..8638d82ae3c 100644
--- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
@@ -22,23 +22,19 @@ describe('UsersChart', () => {
let queryHandler;
const createComponent = ({
- loadingError = false,
- loading = false,
users = [],
additionalData = [],
+ handler = mockQueryResponse({ key: 'users', data: users, additionalData }),
} = {}) => {
- queryHandler = mockQueryResponse({ key: 'users', data: users, loading, additionalData });
+ queryHandler = handler;
- return shallowMount(UsersChart, {
+ wrapper = shallowMount(UsersChart, {
+ apolloProvider: createMockApollo([[usersQuery, queryHandler]]),
props: {
startDate: new Date(2020, 9, 26),
endDate: new Date(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
- apolloProvider: createMockApollo([[usersQuery, queryHandler]]),
- data() {
- return { loadingError };
- },
});
};
@@ -48,7 +44,7 @@ describe('UsersChart', () => {
describe('while loading', () => {
beforeEach(() => {
- wrapper = createComponent({ loading: true });
+ createComponent({ loading: true });
});
it('displays the skeleton loader', () => {
@@ -62,7 +58,7 @@ describe('UsersChart', () => {
describe('without data', () => {
beforeEach(async () => {
- wrapper = createComponent({ users: [] });
+ createComponent({ users: [] });
await nextTick();
});
@@ -81,7 +77,7 @@ describe('UsersChart', () => {
describe('with data', () => {
beforeEach(async () => {
- wrapper = createComponent({ users: mockCountsData2 });
+ createComponent({ users: mockCountsData2 });
await waitForPromises();
});
@@ -102,11 +98,17 @@ describe('UsersChart', () => {
describe('with errors', () => {
beforeEach(async () => {
- wrapper = createComponent({ loadingError: true });
+ createComponent();
await nextTick();
});
- it('renders an error message', () => {
+ it('renders an error message', async () => {
+ createComponent({
+ handler: jest.fn().mockRejectedValue({}),
+ });
+
+ await waitForPromises();
+
expect(findAlert().text()).toBe(
'Could not load the user chart. Please refresh the page to try again.',
);
@@ -124,42 +126,37 @@ describe('UsersChart', () => {
describe('when fetching more data', () => {
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
- wrapper = createComponent({
+ createComponent({
users: mockCountsData2,
additionalData: mockCountsData1,
});
- jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore');
await nextTick();
});
it('requests data twice', () => {
expect(queryHandler).toHaveBeenCalledTimes(2);
});
-
- it('calls fetchMore', () => {
- expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1);
- });
});
describe('when the fetchMore query throws an error', () => {
beforeEach(async () => {
- wrapper = createComponent({
+ createComponent({
users: mockCountsData2,
additionalData: mockCountsData1,
});
- jest
- .spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore')
- .mockImplementation(jest.fn().mockRejectedValue());
await waitForPromises();
});
it('calls fetchMore', () => {
- expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1);
+ expect(queryHandler).toHaveBeenCalledTimes(2);
});
- it('renders an error message', () => {
+ it('renders an error message', async () => {
+ createComponent({ handler: jest.fn().mockRejectedValue({}) });
+ await waitForPromises();
+
expect(findAlert().text()).toBe(
'Could not load the user chart. Please refresh the page to try again.',
);
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index b2ecfeb8394..a6e08e1cf4b 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import projects from 'test_fixtures/api/users/projects/get.json';
import followers from 'test_fixtures/api/users/followers/get.json';
+import following from 'test_fixtures/api/users/following/get.json';
import {
followUser,
unfollowUser,
@@ -9,6 +10,7 @@ import {
updateUserStatus,
getUserProjects,
getUserFollowers,
+ getUserFollowing,
} from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -131,4 +133,23 @@ describe('~/api/user_api', () => {
expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
});
});
+
+ describe('getUserFollowing', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const MOCK_USER_ID = 1;
+ const MOCK_PAGE = 2;
+
+ const expectedUrl = `/api/v4/users/${MOCK_USER_ID}/following`;
+ const expectedResponse = { data: following };
+ const params = { page: MOCK_PAGE };
+
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
+
+ await expect(getUserFollowing(MOCK_USER_ID, params)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 159e36c1364..b6042b4aa81 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -41,9 +41,11 @@ describe('Batch comments draft note component', () => {
},
});
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
+ jest.spyOn(store, 'dispatch').mockImplementation();
};
+ const findNoteableNote = () => wrapper.findComponent(NoteableNote);
+
beforeEach(() => {
store = createStore();
draft = createDraft();
@@ -53,32 +55,28 @@ describe('Batch comments draft note component', () => {
createComponent();
expect(wrapper.findComponent(GlBadge).exists()).toBe(true);
- const note = wrapper.findComponent(NoteableNote);
-
- expect(note.exists()).toBe(true);
- expect(note.props().note).toEqual(draft);
+ expect(findNoteableNote().exists()).toBe(true);
+ expect(findNoteableNote().props('note')).toEqual(draft);
});
describe('update', () => {
it('dispatches updateDraft', async () => {
createComponent();
- const note = wrapper.findComponent(NoteableNote);
-
- note.vm.$emit('handleEdit');
+ findNoteableNote().vm.$emit('handleEdit');
await nextTick();
const formData = {
note: draft,
noteText: 'a',
resolveDiscussion: false,
+ callback: jest.fn(),
+ parentElement: wrapper.vm.$el,
+ errorCallback: jest.fn(),
};
- note.vm.$emit('handleUpdateNote', formData);
+ findNoteableNote().vm.$emit('handleUpdateNote', formData);
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
- 'batchComments/updateDraft',
- formData,
- );
+ expect(store.dispatch).toHaveBeenCalledWith('batchComments/updateDraft', formData);
});
});
@@ -87,18 +85,15 @@ describe('Batch comments draft note component', () => {
createComponent();
jest.spyOn(window, 'confirm').mockImplementation(() => true);
- const note = wrapper.findComponent(NoteableNote);
-
- note.vm.$emit('handleDeleteNote', draft);
+ findNoteableNote().vm.$emit('handleDeleteNote', draft);
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('batchComments/deleteDraft', draft);
+ expect(store.dispatch).toHaveBeenCalledWith('batchComments/deleteDraft', draft);
});
});
describe('quick actions', () => {
it('renders referenced commands', async () => {
- createComponent();
- wrapper.setProps({
+ createComponent({
draft: {
...draft,
references: {
@@ -116,22 +111,27 @@ describe('Batch comments draft note component', () => {
});
describe('multiline comments', () => {
- describe.each`
- desc | props | event | expectedCalls
- ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
- ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
- ${'without `draft.position`'} | ${{}} | ${'mouseenter'} | ${[]}
- ${'without `draft.position`'} | ${{}} | ${'mouseleave'} | ${[]}
- `('$desc', ({ props, event, expectedCalls }) => {
- beforeEach(() => {
- createComponent({ draft: { ...draft, ...props } });
- jest.spyOn(store, 'dispatch');
- });
+ it(`calls store with draft.position with mouseenter`, () => {
+ createComponent({ draft: { ...draft, ...draftWithLineRange } });
+ findNoteableNote().trigger('mouseenter');
- it(`calls store ${expectedCalls.length} times on ${event}`, () => {
- wrapper.element.dispatchEvent(new MouseEvent(event, { bubbles: true }));
- expect(store.dispatch.mock.calls).toEqual(expectedCalls);
- });
+ expect(store.dispatch).toHaveBeenCalledWith('setSelectedCommentPositionHover', LINE_RANGE);
+ });
+
+ it(`calls store with draft.position and mouseleave`, () => {
+ createComponent({ draft: { ...draft, ...draftWithLineRange } });
+ findNoteableNote().trigger('mouseleave');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setSelectedCommentPositionHover');
+ });
+
+ it(`does not call store without draft position`, () => {
+ createComponent({ draft });
+
+ findNoteableNote().trigger('mouseenter');
+ findNoteableNote().trigger('mouseleave');
+
+ expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 5c33df882bf..7e2ff7f786f 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
+import { mockTracking } from 'helpers/tracking_helper';
jest.mock('~/autosave');
@@ -10,9 +11,11 @@ Vue.use(Vuex);
let wrapper;
let publishReview;
+let trackingSpy;
function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
publishReview = jest.fn();
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
const store = new Vuex.Store({
getters: {
@@ -69,6 +72,20 @@ describe('Batch comments submit dropdown', () => {
});
});
+ it('tracks submit action', () => {
+ factory();
+
+ findCommentTextarea().setValue('Hello world');
+
+ findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'MergeRequest_review',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
+
it('switches to the overview tab after submit', async () => {
window.mrTabs = { tabShown: jest.fn() };
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 521bbf06b02..824b2a296c6 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
@@ -1,10 +1,15 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
import service from '~/batch_comments/services/drafts_service';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { UPDATE_COMMENT_FORM } from '~/notes/i18n';
+
+jest.mock('~/alert');
describe('Batch comments store actions', () => {
let res = {};
@@ -44,15 +49,15 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
+ const commit = jest.fn();
+
mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- return testAction(
- actions.addDraftToDiscussion,
- { endpoint: TEST_HOST, data: 'test' },
- null,
- [],
- [],
- );
+ return actions
+ .addDraftToDiscussion({ commit }, { endpoint: TEST_HOST, data: 'test' })
+ .catch(() => {
+ expect(commit).not.toHaveBeenCalledWith('ADD_NEW_DRAFT', expect.anything());
+ });
});
});
@@ -84,15 +89,13 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
+ const commit = jest.fn();
+
mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- return testAction(
- actions.createNewDraft,
- { endpoint: TEST_HOST, data: 'test' },
- null,
- [],
- [],
- );
+ return actions.createNewDraft({ commit }, { endpoint: TEST_HOST, data: 'test' }).catch(() => {
+ expect(commit).not.toHaveBeenCalledWith('ADD_NEW_DRAFT', expect.anything());
+ });
});
});
@@ -239,8 +242,6 @@ describe('Batch comments store actions', () => {
params = { note: { id: 1 }, noteText: 'test' };
});
- afterEach(() => jest.clearAllMocks());
-
it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', () => {
return actions.updateDraft(context, { ...params, callback() {} }).then(() => {
expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 });
@@ -267,6 +268,28 @@ describe('Batch comments store actions', () => {
expect(service.update.mock.calls[0][1].position).toBe(expectation);
});
});
+
+ describe('when updating a draft returns an error', () => {
+ const errorCallback = jest.fn();
+ const flashContainer = null;
+ const error = 'server error';
+
+ beforeEach(async () => {
+ service.update.mockRejectedValue({ response: { data: { errors: error } } });
+ await actions.updateDraft(context, { ...params, flashContainer, errorCallback });
+ });
+
+ it('renders an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(UPDATE_COMMENT_FORM.error, { reason: error }),
+ parent: flashContainer,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalledTimes(1);
+ });
+ });
});
describe('expandAllDiscussions', () => {
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 995e4219ae3..c7f4fce0e4c 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -1,11 +1,18 @@
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockClient } from 'helpers/mock_apollo_helper';
import installGlEmojiElement from '~/behaviors/gl_emoji';
import { EMOJI_VERSION } from '~/emoji';
+import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
import * as EmojiUnicodeSupport from '~/emoji/support';
+let mockClient;
+
jest.mock('~/emoji/support');
+jest.mock('~/lib/graphql', () => {
+ return () => mockClient;
+});
describe('gl_emoji', () => {
const emojiData = {
@@ -36,101 +43,144 @@ describe('gl_emoji', () => {
return div.firstElementChild;
}
- beforeEach(async () => {
- await initEmojiMock(emojiData);
- });
-
afterEach(() => {
clearEmojiMock();
document.body.innerHTML = '';
});
- describe.each([
- [
- 'bomb emoji just with name attribute',
- '<gl-emoji data-name="bomb"></gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
- ],
- [
- 'bomb emoji with name attribute and unicode version',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
- ],
- [
- 'bomb emoji with sprite fallback',
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
- ],
- [
- 'bomb emoji with image fallback',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>',
- ],
- [
- 'invalid emoji',
- '<gl-emoji data-name="invalid_emoji"></gl-emoji>',
- '<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
- `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
- ],
- [
- 'custom emoji with image fallback',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
- ],
- ])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
- it(`renders correctly with emoji support`, async () => {
- jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
- const glEmojiElement = markupToDomElement(markup);
+ describe('standard emoji', () => {
+ beforeEach(async () => {
+ await initEmojiMock(emojiData);
+ });
+
+ describe.each([
+ [
+ 'bomb emoji just with name attribute',
+ '<gl-emoji data-name="bomb"></gl-emoji>',
+ '<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" align="absmiddle"></gl-emoji>`,
+ ],
+ [
+ 'bomb emoji with name attribute and unicode version',
+ '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
+ '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" align="absmiddle"></gl-emoji>`,
+ ],
+ [
+ 'bomb emoji with sprite fallback',
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
+ ],
+ [
+ 'bomb emoji with image fallback',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" align="absmiddle"></gl-emoji>',
+ ],
+ [
+ 'invalid emoji',
+ '<gl-emoji data-name="invalid_emoji"></gl-emoji>',
+ '<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
+ `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" align="absmiddle"></gl-emoji>`,
+ ],
+ [
+ 'custom emoji with image fallback',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" align="absmiddle"></gl-emoji>',
+ ],
+ ])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
+ it(`renders correctly with emoji support`, async () => {
+ jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
+ const glEmojiElement = markupToDomElement(markup);
+
+ await waitForPromises();
+
+ expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
+ });
+
+ it(`renders correctly without emoji support`, async () => {
+ jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
+ const glEmojiElement = markupToDomElement(markup);
+
+ await waitForPromises();
+
+ expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
+ });
+ });
+
+ it('escapes gl-emoji name', async () => {
+ const glEmojiElement = markupToDomElement(
+ "<gl-emoji data-name='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'>abc</gl-emoji>",
+ );
await waitForPromises();
- expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
+ expect(glEmojiElement.outerHTML).toBe(
+ '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" align="absmiddle"></gl-emoji>',
+ );
});
- it(`renders correctly without emoji support`, async () => {
+ it('Adds sprite CSS if emojis are not supported', async () => {
+ const testPath = '/test-path.css';
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
- const glEmojiElement = markupToDomElement(markup);
+ window.gon.emoji_sprites_css_path = testPath;
+ expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
+ expect(window.gon.emoji_sprites_css_added).toBe(undefined);
+
+ markupToDomElement(
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
+ );
await waitForPromises();
- expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
+ expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
+ '<link rel="stylesheet" href="/test-path.css">',
+ );
+ expect(window.gon.emoji_sprites_css_added).toBe(true);
});
});
- it('escapes gl-emoji name', async () => {
- const glEmojiElement = markupToDomElement(
- "<gl-emoji data-name='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'>abc</gl-emoji>",
- );
-
- await waitForPromises();
+ describe('custom emoji', () => {
+ beforeEach(async () => {
+ mockClient = createMockClient([
+ [
+ customEmojiQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ group: {
+ id: 1,
+ customEmoji: {
+ nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+
+ window.gon = { features: { customEmoji: true } };
+ document.body.dataset.groupFullPath = 'test-group';
+
+ await initEmojiMock(emojiData);
+ });
- expect(glEmojiElement.outerHTML).toBe(
- '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>',
- );
- });
+ afterEach(() => {
+ window.gon = {};
+ delete document.body.dataset.groupFullPath;
+ });
- it('Adds sprite CSS if emojis are not supported', async () => {
- const testPath = '/test-path.css';
- jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
- window.gon.emoji_sprites_css_path = testPath;
+ it('renders custom emoji', async () => {
+ const glEmojiElement = markupToDomElement('<gl-emoji data-name="parrot"></gl-emoji>');
- expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
- expect(window.gon.emoji_sprites_css_added).toBe(undefined);
+ await waitForPromises();
- markupToDomElement(
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
- );
- await waitForPromises();
+ const img = glEmojiElement.querySelector('img');
- expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
- '<link rel="stylesheet" href="/test-path.css">',
- );
- expect(window.gon.emoji_sprites_css_added).toBe(true);
+ expect(glEmojiElement.dataset.unicodeVersion).toBe('custom');
+ expect(img.getAttribute('src')).toBe('parrot.gif');
+ });
});
});
diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js
index 220ad874b47..0bbb92282e5 100644
--- a/spec/frontend/behaviors/markdown/render_gfm_spec.js
+++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js
@@ -1,7 +1,4 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-import renderMetrics from '~/behaviors/markdown/render_metrics';
-
-jest.mock('~/behaviors/markdown/render_metrics');
describe('renderGFM', () => {
it('handles a missing element', () => {
@@ -9,27 +6,4 @@ describe('renderGFM', () => {
renderGFM();
}).not.toThrow();
});
-
- describe('remove_monitor_metrics flag', () => {
- let metricsElement;
-
- beforeEach(() => {
- window.gon = { features: { removeMonitorMetrics: true } };
- metricsElement = document.createElement('div');
- metricsElement.setAttribute('class', '.js-render-metrics');
- });
-
- it('renders metrics when the flag is disabled', () => {
- window.gon.features = { features: { removeMonitorMetrics: false } };
- renderGFM(metricsElement);
-
- expect(renderMetrics).toHaveBeenCalled();
- });
-
- it('does not render metrics when the flag is enabled', () => {
- renderGFM(metricsElement);
-
- expect(renderMetrics).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js
deleted file mode 100644
index ab81ed6b8f0..00000000000
--- a/spec/frontend/behaviors/markdown/render_metrics_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { TEST_HOST } from 'helpers/test_constants';
-import renderMetrics from '~/behaviors/markdown/render_metrics';
-
-const mockEmbedGroup = jest.fn();
-
-jest.mock('vue', () => ({ extend: () => mockEmbedGroup }));
-jest.mock('~/monitoring/components/embeds/embed_group.vue', () => jest.fn());
-jest.mock('~/monitoring/stores/embed_group/', () => ({ createStore: jest.fn() }));
-
-const getElements = () => Array.from(document.getElementsByClassName('js-render-metrics'));
-
-describe('Render metrics for Gitlab Flavoured Markdown', () => {
- it('does nothing when no elements are found', () => {
- return renderMetrics([]).then(() => {
- expect(mockEmbedGroup).not.toHaveBeenCalled();
- });
- });
-
- it('renders a vue component when elements are found', () => {
- document.body.innerHTML = `<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}"></div>`;
-
- return renderMetrics(getElements()).then(() => {
- expect(mockEmbedGroup).toHaveBeenCalledTimes(1);
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }),
- );
- });
- });
-
- it('takes sibling metrics and groups them under a shared parent', () => {
- document.body.innerHTML = `
- <p><span>Hello</span></p>
- <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/1"></div>
- <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/2"></div>
- <p><span>Hello</span></p>
- <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/3"></div>
- `;
-
- return renderMetrics(getElements()).then(() => {
- expect(mockEmbedGroup).toHaveBeenCalledTimes(2);
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }),
- );
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }),
- );
- });
- });
-});
diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index b2e1a29b84f..de39a8f688a 100644
--- a/spec/frontend/blob/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -1,5 +1,4 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
-import $ from 'jquery';
import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter';
@@ -9,11 +8,15 @@ describe('LineHighlighter', () => {
const testContext = {};
const clickLine = (number, eventData = {}) => {
- if ($.isEmptyObject(eventData)) {
- return $(`#L${number}`).click();
+ if (Object.keys(eventData).length === 0) {
+ return document.querySelector(`#L${number}`).click();
}
- const e = $.Event('click', eventData);
- return $(`#L${number}`).trigger(e);
+ const e = new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ ...eventData,
+ });
+ return document.querySelector(`#L${number}`).dispatchEvent(e);
};
beforeEach(() => {
@@ -35,32 +38,30 @@ describe('LineHighlighter', () => {
it('highlights one line given in the URL hash', () => {
new LineHighlighter({ hash: '#L13' });
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
it('highlights one line given in the URL hash with given CSS class name', () => {
const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' });
expect(hiliter.highlightLineClass).toBe('hilite');
- expect($('#LC13')).toHaveClass('hilite');
- expect($('#LC13')).not.toHaveClass('hll');
+ expect(document.querySelector('#LC13').classList).toContain('hilite');
+ expect(document.querySelector('#LC13').classList).not.toContain('hll');
});
it('highlights a range of lines given in the URL hash', () => {
new LineHighlighter({ hash: '#L5-25' });
- expect($(`.${testContext.css}`).length).toBe(21);
for (let line = 5; line <= 25; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
it('highlights a range of lines given in the URL hash using GitHub format', () => {
new LineHighlighter({ hash: '#L5-L25' });
- expect($(`.${testContext.css}`).length).toBe(21);
for (let line = 5; line <= 25; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
@@ -74,11 +75,13 @@ describe('LineHighlighter', () => {
it('discards click events', () => {
const clickSpy = jest.fn();
- $('a[data-line-number]').click(clickSpy);
+ document.querySelectorAll('a[data-line-number]').forEach((el) => {
+ el.addEventListener('click', clickSpy);
+ });
clickLine(13);
- expect(clickSpy.mock.calls[0][0].isDefaultPrevented()).toEqual(true);
+ expect(clickSpy.mock.calls[0][0].defaultPrevented).toEqual(true);
});
it('handles garbage input from the hash', () => {
@@ -101,27 +104,19 @@ describe('LineHighlighter', () => {
});
describe('clickHandler', () => {
- it('handles clicking on a child icon element', () => {
- const spy = jest.spyOn(testContext.class, 'setHash');
- $('#L13 [data-testid="link-icon"]').mousedown().click();
-
- expect(spy).toHaveBeenCalledWith(13);
- expect($('#LC13')).toHaveClass(testContext.css);
- });
-
describe('without shiftKey', () => {
it('highlights one line when clicked', () => {
clickLine(13);
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
it('unhighlights previously highlighted lines', () => {
clickLine(13);
clickLine(20);
- expect($('#LC13')).not.toHaveClass(testContext.css);
- expect($('#LC20')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).not.toContain(testContext.css);
+ expect(document.querySelector('#LC20').classList).toContain(testContext.css);
});
it('sets the hash', () => {
@@ -138,6 +133,8 @@ describe('LineHighlighter', () => {
clickLine(13);
clickLine(20, {
shiftKey: true,
+ bubbles: true,
+ cancelable: true,
});
expect(spy).toHaveBeenCalledWith(13);
@@ -150,8 +147,8 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($('#LC13')).toHaveClass(testContext.css);
- expect($(`.${testContext.css}`).length).toBe(1);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(1);
});
it('sets the hash', () => {
@@ -171,9 +168,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 15; line <= 20; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
@@ -183,9 +180,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 5; line <= 10; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
});
@@ -205,9 +202,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 5; line <= 10; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
@@ -216,9 +213,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 10; line <= 15; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
});
@@ -251,13 +248,13 @@ describe('LineHighlighter', () => {
it('highlights the specified line', () => {
testContext.subject(13);
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
it('accepts a String-based number', () => {
testContext.subject('13');
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
});
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 9ab20fc2cd7..1bdc54723ce 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -61,7 +61,6 @@ describe('Blob Editing', () => {
});
afterEach(() => {
mock.restore();
- jest.clearAllMocks();
unuseMock.mockClear();
useMock.mockClear();
resetHTMLFixture();
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index a925f752f5e..36556ba00af 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -92,6 +92,7 @@ describe('Board card component', () => {
isEpicBoard,
issuableType: TYPE_ISSUE,
isGroupBoard,
+ isApolloBoard: false,
},
});
};
@@ -111,7 +112,6 @@ describe('Board card component', () => {
afterEach(() => {
store = null;
- jest.clearAllMocks();
});
it('renders issue title', () => {
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 3d6e4c18f51..e7624437ac5 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -4,7 +4,9 @@ import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import BoardApp from '~/boards/components/board_app.vue';
+import eventHub from '~/boards/eventhub';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { rawIssue, boardListsQueryResponse } from '../mock_data';
@@ -93,5 +95,14 @@ describe('BoardApp', () => {
expect(wrapper.classes()).not.toContain('is-compact');
});
+
+ 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_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 9260718a94b..0a2a78479fb 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
-import eventHub from '~/boards/eventhub';
+
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
@@ -182,15 +182,6 @@ describe('BoardContent', () => {
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);
- });
-
it('reorders lists', async () => {
const movableListsOrder = [mockLists[0].id, mockLists[1].id];
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index ad2674f9d3b..0c9e1b4646e 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,4 +1,4 @@
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { GlButtonGroup } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
@@ -93,18 +93,17 @@ describe('Board List Header Component', () => {
...injectedProps,
},
stubs: {
- GlDisclosureDropdown,
- GlDisclosureDropdownItem,
+ GlButtonGroup,
},
});
};
- const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const isCollapsed = () => wrapper.vm.list.collapsed;
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
- const findNewIssueButton = () => wrapper.findByTestId('newIssueBtn');
- const findSettingsButton = () => wrapper.findByTestId('settingsBtn');
+ const findNewIssueButton = () => wrapper.findByTestId('new-issue-btn');
+ const findSettingsButton = () => wrapper.findByTestId('settings-btn');
const findBoardListHeader = () => wrapper.findByTestId('board-list-header');
it('renders border when label color is present', () => {
@@ -131,13 +130,13 @@ describe('Board List Header Component', () => {
it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findDropdown().exists()).toBe(false);
+ expect(findButtonGroup().exists()).toBe(false);
});
it.each(hasAddButton)('does render when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findDropdown().exists()).toBe(true);
+ expect(findButtonGroup().exists()).toBe(true);
expect(findNewIssueButton().exists()).toBe(true);
});
@@ -146,7 +145,7 @@ describe('Board List Header Component', () => {
currentUserId: null,
});
- expect(findDropdown().exists()).toBe(false);
+ expect(findButtonGroup().exists()).toBe(false);
});
});
@@ -156,20 +155,20 @@ describe('Board List Header Component', () => {
it.each(hasSettings)('does render for List Type `%s`', (listType) => {
createComponent({ listType });
- expect(findDropdown().exists()).toBe(true);
+ expect(findButtonGroup().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);
+ expect(findButtonGroup().exists()).toBe(false);
});
it('renders dropdown but not the Settings button when ListType `backlog`', () => {
createComponent({ listType: ListType.backlog });
- expect(findDropdown().exists()).toBe(true);
+ expect(findButtonGroup().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 651d1daee52..a1088f1e8f7 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -1,25 +1,49 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import BoardNewItem from '~/boards/components/board_new_item.vue';
import ProjectSelect from '~/boards/components/project_select.vue';
import eventHub from '~/boards/eventhub';
-
-import { mockList, mockGroupProjects, mockIssue, mockIssue2 } from '../mock_data';
+import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
+import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+
+import {
+ mockList,
+ mockGroupProjects,
+ mockIssue,
+ mockIssue2,
+ mockProjectBoardResponse,
+ mockGroupBoardResponse,
+} from '../mock_data';
Vue.use(Vuex);
+Vue.use(VueApollo);
const addListNewIssuesSpy = jest.fn().mockResolvedValue();
const mockActions = { addListNewIssue: addListNewIssuesSpy };
+const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
+const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
+
+const mockApollo = createMockApollo([
+ [projectBoardQuery, projectBoardQueryHandlerSuccess],
+ [groupBoardQuery, groupBoardQueryHandlerSuccess],
+]);
+
const createComponent = ({
- state = { selectedProject: mockGroupProjects[0] },
+ state = {},
actions = mockActions,
getters = { getBoardItemsByList: () => () => [] },
isGroupBoard = true,
+ data = { selectedProject: mockGroupProjects[0] },
+ provide = {},
} = {}) =>
shallowMount(BoardNewIssue, {
+ apolloProvider: mockApollo,
store: new Vuex.Store({
state,
actions,
@@ -27,13 +51,19 @@ const createComponent = ({
}),
propsData: {
list: mockList,
+ boardId: 'gid://gitlab/Board/1',
},
+ data: () => data,
provide: {
groupId: 1,
fullPath: mockGroupProjects[0].fullPath,
weightFeatureAvailable: false,
boardWeight: null,
isGroupBoard,
+ boardType: 'group',
+ isEpicBoard: false,
+ isApolloBoard: false,
+ ...provide,
},
stubs: {
BoardNewItem,
@@ -137,4 +167,33 @@ describe('Issue boards new issue form', () => {
expect(projectSelect.exists()).toBe(false);
});
});
+
+ describe('Apollo boards', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ `(
+ 'fetches $boardType board and emits addNewIssue event',
+ async ({ boardType, queryHandler, notCalledHandler }) => {
+ wrapper = createComponent({
+ provide: {
+ boardType,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isApolloBoard: true,
+ },
+ });
+
+ await nextTick();
+ findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ expect(wrapper.emitted('addNewIssue')[0][0]).toMatchObject({ title: 'Foo' });
+ },
+ );
+ });
});
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index b1e14be8ceb..affe1260c66 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -90,10 +90,6 @@ describe('BoardSettingsSidebar', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findRemoveButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
it('finds a MountingPortal component', () => {
createComponent();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 447aacd9cea..8235c3e4194 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -836,6 +836,7 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn)
type: TOKEN_TYPE_ASSIGNEE,
operators: OPERATORS_IS_NOT,
token: UserToken,
+ dataType: 'user',
unique: true,
fetchUsers,
preloadedUsers: [],
@@ -847,6 +848,7 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn)
operators: OPERATORS_IS_NOT,
symbol: '@',
token: UserToken,
+ dataType: 'user',
unique: true,
fetchUsers,
preloadedUsers: [],
@@ -1040,4 +1042,43 @@ export const destroyBoardListMutationResponse = {
},
};
+export const mockProjects = [
+ {
+ id: 'gid://gitlab/Project/1',
+ name: 'Gitlab Shell',
+ nameWithNamespace: 'Gitlab Org / Gitlab Shell',
+ fullPath: 'gitlab-org/gitlab-shell',
+ archived: false,
+ __typename: 'Project',
+ },
+ {
+ id: 'gid://gitlab/Project/2',
+ name: 'Gitlab Test',
+ nameWithNamespace: 'Gitlab Org / Gitlab Test',
+ fullPath: 'gitlab-org/gitlab-test',
+ archived: true,
+ __typename: 'Project',
+ },
+];
+
+export const mockGroupProjectsResponse = (projects = mockProjects) => ({
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ projects: {
+ nodes: projects,
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'abc',
+ endCursor: 'bcd',
+ __typename: 'PageInfo',
+ },
+ __typename: 'ProjectConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+});
+
export const DEFAULT_COLOR = '#1068bf';
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index b4308b38947..f1daccfadda 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -1,17 +1,19 @@
import { GlCollapsibleListbox, GlListboxItem, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
+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 groupProjectsQuery from '~/boards/graphql/group_projects.query.graphql';
import ProjectSelect from '~/boards/components/project_select.vue';
-import defaultState from '~/boards/stores/state';
-import { mockActiveGroupProjects, mockList } from './mock_data';
+import { mockList, mockGroupProjectsResponse, mockProjects } from './mock_data';
-const mockProjectsList1 = mockActiveGroupProjects.slice(0, 1);
+Vue.use(VueApollo);
describe('ProjectSelect component', () => {
let wrapper;
- let store;
+ let mockApollo;
const findLabel = () => wrapper.find("[data-testid='header-label']");
const findGlCollapsibleListBox = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -26,77 +28,54 @@ describe('ProjectSelect component', () => {
const findInMenuLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
const findEmptySearchMessage = () => wrapper.find("[data-testid='listbox-no-results-text']");
- const createStore = ({ state, activeGroupProjects }) => {
- Vue.use(Vuex);
-
- store = new Vuex.Store({
- state: {
- defaultState,
- groupProjectsFlags: {
- isLoading: false,
- pageInfo: {
- hasNextPage: false,
- },
- },
- ...state,
- },
- actions: {
- fetchGroupProjects: jest.fn(),
- setSelectedProject: jest.fn(),
- },
- getters: {
- activeGroupProjects: () => activeGroupProjects,
- },
- });
- };
-
- const createWrapper = ({ state = {}, activeGroupProjects = [] } = {}) => {
- createStore({
- state,
- activeGroupProjects,
- });
+ const projectsQueryHandler = jest.fn().mockResolvedValue(mockGroupProjectsResponse());
+ const emptyProjectsQueryHandler = jest.fn().mockResolvedValue(mockGroupProjectsResponse([]));
- wrapper = mount(ProjectSelect, {
+ const createWrapper = ({ queryHandler = projectsQueryHandler, selectedProject = {} } = {}) => {
+ mockApollo = createMockApollo([[groupProjectsQuery, queryHandler]]);
+ wrapper = mountExtended(ProjectSelect, {
+ apolloProvider: mockApollo,
propsData: {
list: mockList,
+ selectedProject,
},
- store,
provide: {
groupId: 1,
+ fullPath: 'gitlab-org',
},
attachTo: document.body,
});
};
- it('displays a header title', () => {
- createWrapper();
-
- expect(findLabel().text()).toBe('Projects');
- });
-
- it('renders a default dropdown text', () => {
- createWrapper();
-
- expect(findGlCollapsibleListBox().exists()).toBe(true);
- expect(findGlCollapsibleListBox().text()).toContain('Select a project');
- });
-
describe('when mounted', () => {
- it('displays a loading icon while projects are being fetched', async () => {
+ beforeEach(() => {
createWrapper();
+ });
+ it('displays a loading icon while projects are being fetched', async () => {
expect(findGlDropdownLoadingIcon().exists()).toBe(true);
- await nextTick();
+ await waitForPromises();
expect(findGlDropdownLoadingIcon().exists()).toBe(false);
+ expect(projectsQueryHandler).toHaveBeenCalled();
+ });
+
+ it('displays a header title', () => {
+ expect(findLabel().text()).toBe('Projects');
+ });
+
+ it('renders a default dropdown text', () => {
+ expect(findGlCollapsibleListBox().exists()).toBe(true);
+ expect(findGlCollapsibleListBox().text()).toContain('Select a project');
});
});
describe('when dropdown menu is open', () => {
describe('by default', () => {
- beforeEach(() => {
- createWrapper({ activeGroupProjects: mockActiveGroupProjects });
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
});
it('shows GlListboxSearchInput with placeholder text', () => {
@@ -106,7 +85,7 @@ describe('ProjectSelect component', () => {
it("displays the fetched project's name", () => {
expect(findFirstGlDropdownItem().exists()).toBe(true);
- expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name);
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjects[0].name);
});
it("doesn't render loading icon in the menu", () => {
@@ -119,33 +98,31 @@ describe('ProjectSelect component', () => {
});
describe('when no projects are being returned', () => {
- it('renders empty search result message', () => {
- createWrapper();
+ it('renders empty search result message', async () => {
+ createWrapper({ queryHandler: emptyProjectsQueryHandler });
+ await waitForPromises();
expect(findEmptySearchMessage().exists()).toBe(true);
});
});
describe('when a project is selected', () => {
- beforeEach(() => {
- createWrapper({ activeGroupProjects: mockProjectsList1 });
-
- findFirstGlDropdownItem().find('li').trigger('click');
+ beforeEach(async () => {
+ createWrapper({ selectedProject: mockProjects[0] });
+ await waitForPromises();
});
it('renders the name of the selected project', () => {
expect(findGlCollapsibleListBox().find('.gl-new-dropdown-button-text').text()).toBe(
- mockProjectsList1[0].name,
+ mockProjects[0].name,
);
});
});
describe('when projects are loading', () => {
- beforeEach(() => {
- createWrapper({ state: { groupProjectsFlags: { isLoading: true } } });
- });
-
- it('displays and hides gl-loading-icon while and after fetching data', () => {
+ it('displays and hides gl-loading-icon while and after fetching data', async () => {
+ createWrapper();
+ await nextTick();
expect(findInMenuLoadingIcon().isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index f3800ce8324..a2961fb1302 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1541,8 +1541,8 @@ describe('addListNewIssue', () => {
it('should add board scope to the issue being created', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
- createIssue: {
- issue: mockIssue,
+ createIssuable: {
+ issuable: mockIssue,
errors: [],
},
},
@@ -1600,8 +1600,8 @@ describe('addListNewIssue', () => {
it('dispatches a correct set of mutations', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
- createIssue: {
- issue: mockIssue,
+ createIssuable: {
+ issuable: mockIssue,
errors: [],
},
},
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
index 9db6a523dec..4da56a865d5 100644
--- a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -5,7 +5,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
<gl-base-dropdown-stub
category="tertiary"
class="gl-disclosure-dropdown gl-display-none gl-md-display-block!"
- data-qa-selector="delete_merged_branches_dropdown_button"
icon="ellipsis_v"
nocaret="true"
offset="[object Object]"
@@ -34,7 +33,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
<b-button-stub
class="gl-display-block gl-md-display-none! gl-button btn-danger-secondary"
- data-qa-selector="delete_merged_branches_button"
+ data-testid="delete-merged-branches-button"
size="md"
tag="button"
type="button"
@@ -100,7 +99,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
aria-labelledby="input-label"
autocomplete="off"
class="gl-form-input gl-mt-2 gl-form-input-sm"
- data-qa-selector="delete_merged_branches_input"
debounce="0"
formatter="[Function]"
type="text"
@@ -146,7 +144,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
<b-button-stub
class="gl-button"
- data-qa-selector="delete_merged_branches_confirmation_button"
data-testid="delete-merged-branches-confirmation-button"
disabled="true"
size="md"
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
index 3e47e76622d..3319ed13004 100644
--- a/spec/frontend/branches/components/delete_merged_branches_spec.js
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -37,7 +37,7 @@ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
};
const findDeleteButton = () =>
- wrapper.findComponent('[data-qa-selector="delete_merged_branches_button"]');
+ wrapper.findComponent('[data-testid="delete-merged-branches-button"]');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmationButton = () =>
wrapper.findByTestId('delete-merged-branches-confirmation-button');
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 1937e3b34b7..64227872af3 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
@@ -1,4 +1,10 @@
-import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
+import {
+ GlListboxItem,
+ GlCollapsibleListbox,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlIcon,
+} from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
@@ -10,6 +16,7 @@ describe('Ci environments dropdown', () => {
const defaultProps = {
areEnvironmentsLoading: false,
environments: envs,
+ hasEnvScopeQuery: false,
selectedEnvironmentScope: '',
};
@@ -19,19 +26,15 @@ describe('Ci environments dropdown', () => {
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
- const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
+ const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
wrapper = mountExtended(CiEnvironmentsDropdown, {
propsData: {
...defaultProps,
...props,
},
- provide: {
- glFeatures: {
- ciLimitEnvironmentScope: enableFeatureFlag,
- },
- },
});
findListbox().vm.$emit('search', searchTerm);
@@ -42,6 +45,10 @@ describe('Ci environments dropdown', () => {
createComponent({ searchTerm: 'stable' });
});
+ it('renders dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ });
+
it('renders create button with search term if environments do not contain search term', () => {
const button = findCreateWildcardButton();
expect(button.exists()).toBe(true);
@@ -51,14 +58,14 @@ describe('Ci environments dropdown', () => {
describe('Search term is empty', () => {
describe.each`
- featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
- ${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
- ${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
+ hasEnvScopeQuery | status | defaultEnvStatus | firstItemValue | envIndices
+ ${true} | ${'exists'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
+ ${false} | ${'does not exist'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
`(
- 'when ciLimitEnvironmentScope feature flag is $flagStatus',
- ({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
+ 'when query for fetching environment scope $status',
+ ({ defaultEnvStatus, firstItemValue, hasEnvScopeQuery, envIndices }) => {
beforeEach(() => {
- createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
+ createComponent({ props: { environments: envs, hasEnvScopeQuery } });
});
it(`${defaultEnvStatus} * in listbox`, () => {
@@ -91,7 +98,7 @@ describe('Ci environments dropdown', () => {
});
});
- describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
+ describe('when environments are not fetched via graphql', () => {
const currentEnv = envs[2];
beforeEach(() => {
@@ -118,11 +125,15 @@ describe('Ci environments dropdown', () => {
});
});
- describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
+ describe('when fetching environments via graphql', () => {
const currentEnv = envs[2];
beforeEach(() => {
- createComponent({ enableFeatureFlag: true });
+ createComponent({ props: { hasEnvScopeQuery: true } });
+ });
+
+ it('renders dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
});
it('renders environments passed down to it', async () => {
@@ -131,6 +142,22 @@ describe('Ci environments dropdown', () => {
expect(findAllListboxItems()).toHaveLength(envs.length);
});
+ it('renders dropdown loading icon while fetch query is loading', () => {
+ createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+
+ expect(findListbox().props('loading')).toBe(true);
+ expect(findListbox().props('searching')).toBe(false);
+ expect(findDropdownDivider().exists()).toBe(false);
+ });
+
+ it('renders search loading icon while search query is loading and dropdown is open', async () => {
+ createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+ await findListbox().vm.$emit('shown');
+
+ expect(findListbox().props('loading')).toBe(false);
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
it('emits event when searching', async () => {
expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
@@ -140,12 +167,6 @@ describe('Ci environments dropdown', () => {
expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
});
- it('renders loading icon while search query is loading', () => {
- createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
it('displays note about max environments shown', () => {
expect(findMaxEnvNote().exists()).toBe(true);
expect(findMaxEnvNote().text()).toContain(String(ENVIRONMENT_QUERY_LIMIT));
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 7436210fe70..b364f098a3a 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
@@ -9,15 +9,13 @@ import {
DELETE_MUTATION_ACTION,
UPDATE_MUTATION_ACTION,
} from '~/ci/ci_variable_list/constants';
+import getGroupEnvironments from '~/ci/ci_variable_list/graphql/queries/group_environments.query.graphql';
import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
const mockProvide = {
- glFeatures: {
- groupScopedCiVariables: false,
- },
groupPath: '/group',
groupId: 12,
};
@@ -27,9 +25,16 @@ describe('Ci Group Variable wrapper', () => {
const findCiShared = () => wrapper.findComponent(ciVariableShared);
- const createComponent = ({ provide = {} } = {}) => {
+ const createComponent = ({ featureFlags } = {}) => {
wrapper = shallowMount(ciGroupVariables, {
- provide: { ...mockProvide, ...provide },
+ provide: {
+ ...mockProvide,
+ glFeatures: {
+ ciGroupEnvScopeGraphql: false,
+ groupScopedCiVariables: false,
+ ...featureFlags,
+ },
+ },
});
};
@@ -62,10 +67,10 @@ describe('Ci Group Variable wrapper', () => {
});
});
- describe('feature flag', () => {
+ describe('groupScopedCiVariables feature flag', () => {
describe('When enabled', () => {
beforeEach(() => {
- createComponent({ provide: { glFeatures: { groupScopedCiVariables: true } } });
+ createComponent({ featureFlags: { groupScopedCiVariables: true } });
});
it('Passes down `true` to variable shared component', () => {
@@ -75,7 +80,7 @@ describe('Ci Group Variable wrapper', () => {
describe('When disabled', () => {
beforeEach(() => {
- createComponent({ provide: { glFeatures: { groupScopedCiVariables: false } } });
+ createComponent();
});
it('Passes down `false` to variable shared component', () => {
@@ -83,4 +88,26 @@ describe('Ci Group Variable wrapper', () => {
});
});
});
+
+ describe('ciGroupEnvScopeGraphql feature flag', () => {
+ describe('When enabled', () => {
+ beforeEach(() => {
+ createComponent({ featureFlags: { ciGroupEnvScopeGraphql: true } });
+ });
+
+ it('Passes down environments query to variable shared component', () => {
+ expect(findCiShared().props('queryData').environments.query).toBe(getGroupEnvironments);
+ });
+ });
+
+ describe('When disabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Does not pass down environments query to variable shared component', () => {
+ expect(findCiShared().props('queryData').environments).toBe(undefined);
+ });
+ });
+ });
});
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 e9484cfce57..d843646df16 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
@@ -48,6 +48,7 @@ describe('Ci variable modal', () => {
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
+ hasEnvScopeQuery: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variables: [],
@@ -349,14 +350,14 @@ describe('Ci variable modal', () => {
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
- describe('when feature flag is enabled', () => {
+ describe('when query for envioronment scope exists', () => {
beforeEach(() => {
createComponent({
props: {
environments: mockEnvs,
+ hasEnvScopeQuery: true,
variables: mockVariablesWithUniqueScopes(projectString),
},
- provide: { glFeatures: { ciLimitEnvironmentScope: true } },
});
});
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 12ca9a78369..d72cfc5fc14 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
@@ -21,6 +21,7 @@ describe('Ci variable table', () => {
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
+ hasEnvScopeQuery: false,
maxVariableLimit: 5,
pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
@@ -60,6 +61,7 @@ describe('Ci variable table', () => {
areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
+ hasEnvScopeQuery: defaultProps.hasEnvScopeQuery,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
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 f7b90c3da30..6fa1915f3c1 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
@@ -52,6 +52,7 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
+ hasEnvScopeQuery: false,
pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
@@ -219,16 +220,12 @@ describe('Ci Variable Shared Component', () => {
expect(mockEnvironments).toHaveBeenCalled();
});
- describe('when Limit Environment Scope FF is enabled', () => {
+ // applies only to project-level CI variables
+ describe('when environment scope is limited', () => {
beforeEach(async () => {
await createComponentWithApollo({
props: { ...createProjectProps() },
- provide: {
- glFeatures: {
- ciLimitEnvironmentScope: true,
- ciVariablesPages: isVariablePagesEnabled,
- },
- },
+ provide: pagesFeatureFlagProvide,
});
});
@@ -251,26 +248,11 @@ describe('Ci Variable Shared Component', () => {
expect.objectContaining({ search: 'staging' }),
);
});
- });
-
- describe('when Limit Environment Scope FF is disabled', () => {
- beforeEach(async () => {
- await createComponentWithApollo({
- props: { ...createProjectProps() },
- provide: pagesFeatureFlagProvide,
- });
- });
- it('initial query is called with the correct variables', () => {
- expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
- });
+ it('does not show loading icon in table while searching for environments', () => {
+ findCiSettings().vm.$emit('search-environment-scope', 'staging');
- it(`does not refetch environments when search term is present`, async () => {
- expect(mockEnvironments).toHaveBeenCalledTimes(1);
-
- await findCiSettings().vm.$emit('search-environment-scope', 'staging');
-
- expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ expect(findLoadingIcon().exists()).toBe(false);
});
});
});
@@ -532,6 +514,7 @@ describe('Ci Variable Shared Component', () => {
areEnvironmentsLoading: false,
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ hasEnvScopeQuery: props.hasEnvScopeQuery,
pageInfo: defaultProps.pageInfo,
isLoading: false,
maxVariableLimit,
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 9c9c99ad5ea..41dfc0ebfda 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -189,6 +189,7 @@ export const createProjectProps = () => {
componentName: 'ProjectVariable',
entity: 'project',
fullPath: '/namespace/project/',
+ hasEnvScopeQuery: true,
id: 'gid://gitlab/Project/20',
mutationData: {
[ADD_MUTATION_ACTION]: addProjectVariable,
@@ -213,6 +214,7 @@ export const createGroupProps = () => {
componentName: 'GroupVariable',
entity: 'group',
fullPath: '/my-group',
+ hasEnvScopeQuery: false,
id: 'gid://gitlab/Group/20',
mutationData: {
[ADD_MUTATION_ACTION]: addGroupVariable,
@@ -231,6 +233,7 @@ export const createGroupProps = () => {
export const createInstanceProps = () => {
return {
componentName: 'InstanceVariable',
+ hasEnvScopeQuery: false,
entity: '',
mutationData: {
[ADD_MUTATION_ACTION]: addAdminVariable,
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 8834231aaef..7a9b4ffdce8 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
@@ -17,7 +17,8 @@ import {
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
+import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
+
import {
mockCiConfigPath,
mockCiYml,
@@ -253,18 +254,20 @@ describe('Pipeline Editor | Commit section', () => {
describe('when the commit returns a different etag path', () => {
beforeEach(async () => {
createComponentWithApollo();
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
+ jest.spyOn(mockApollo.clients.defaultClient.cache, 'writeQuery');
+
mockMutateCommitData.mockResolvedValue(mockCommitCreateResponseNewEtag);
await submitCommit();
});
- it('calls the client mutation to update the etag', () => {
- // 1:Commit submission, 2:etag update, 3:currentBranch update, 4:lastCommit update
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(4);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenNthCalledWith(2, {
- mutation: updatePipelineEtag,
- variables: {
- pipelineEtag: mockCommitCreateResponseNewEtag.data.commitCreate.commitPipelinePath,
+ it('calls the client mutation to update the etag in the cache', () => {
+ expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith({
+ query: getPipelineEtag,
+ data: {
+ etags: {
+ __typename: 'EtagValues',
+ pipeline: mockCommitCreateResponseNewEtag.data.commitCreate.commitPipelinePath,
+ },
},
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
index edaa96a197a..d40499fae87 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
@@ -49,32 +49,36 @@ describe('Rules item', () => {
findRulesWhenSelect().vm.$emit('input', dummyRulesWhen);
- expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
expect(wrapper.emitted('update-job')[0]).toEqual([
'rules[0].when',
JOB_RULES_WHEN.delayed.value,
]);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'rules[0].start_in',
+ `1 ${JOB_RULES_START_IN.second.value}`,
+ ]);
findRulesStartInNumberInput().vm.$emit('input', dummyRulesStartInNumber);
- expect(wrapper.emitted('update-job')).toHaveLength(2);
- expect(wrapper.emitted('update-job')[1]).toEqual([
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual([
'rules[0].start_in',
`2 ${JOB_RULES_START_IN.second.value}s`,
]);
findRulesStartInUnitSelect().vm.$emit('input', dummyRulesStartInUnit);
- expect(wrapper.emitted('update-job')).toHaveLength(3);
- expect(wrapper.emitted('update-job')[2]).toEqual([
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual([
'rules[0].start_in',
`2 ${dummyRulesStartInUnit}s`,
]);
findRulesAllowFailureCheckBox().vm.$emit('input', dummyRulesAllowFailure);
- expect(wrapper.emitted('update-job')).toHaveLength(4);
- expect(wrapper.emitted('update-job')[3]).toEqual([
+ expect(wrapper.emitted('update-job')).toHaveLength(5);
+ expect(wrapper.emitted('update-job')[4]).toEqual([
'rules[0].allow_failure',
dummyRulesAllowFailure,
]);
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index 639c2dbef4c..bb48d4dc38d 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,14 +1,47 @@
import MockAdapter from 'axios-mock-adapter';
-import { GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import { GlForm, GlLoadingIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql';
+import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers';
+import {
+ createScheduleMutationResponse,
+ updateScheduleMutationResponse,
+ mockSinglePipelineScheduleNode,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ joinPaths: jest.fn().mockReturnValue(''),
+ queryToObject: jest.fn().mockReturnValue({ id: '1' }),
+}));
+
+const {
+ data: {
+ project: {
+ pipelineSchedules: { nodes },
+ },
+ },
+} = mockSinglePipelineScheduleNode;
+
+const schedule = nodes[0];
+const variables = schedule.variables.nodes;
describe('Pipeline schedules form', () => {
let wrapper;
@@ -17,22 +50,36 @@ describe('Pipeline schedules form', () => {
const cron = '';
const dailyLimit = '';
- const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode);
+ const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse);
+ const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse);
+ const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const createMockApolloProvider = (
+ requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]],
+ ) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (mountFn = shallowMountExtended, editing = false, requestHandlers) => {
wrapper = mountFn(PipelineSchedulesForm, {
propsData: {
timezoneData: timezoneDataFixture,
refParam: 'master',
+ editing,
},
provide: {
fullPath: 'gitlab-org/gitlab',
projectId,
defaultBranch,
- cron,
- cronTimezone: '',
dailyLimit,
settingsLink: '',
+ schedulesPath: '/root/ci-project/-/pipeline_schedules',
},
- stubs,
+ apolloProvider: createMockApolloProvider(requestHandlers),
});
};
@@ -43,17 +90,24 @@ describe('Pipeline schedules form', () => {
const findRefSelector = () => wrapper.findComponent(RefSelector);
const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button');
const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
// Variables
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
- beforeEach(() => {
- createComponent();
- });
+ const addVariableToForm = () => {
+ const input = findKeyInputs().at(0);
+ input.element.value = 'test_var_2';
+ input.trigger('change');
+ };
describe('Form elements', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('displays form', () => {
expect(findForm().exists()).toBe(true);
});
@@ -102,19 +156,16 @@ describe('Pipeline schedules form', () => {
it('displays the submit and cancel buttons', () => {
expect(findSubmitButton().exists()).toBe(true);
expect(findCancelButton().exists()).toBe(true);
+ expect(findCancelButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules');
});
});
describe('CI variables', () => {
let mock;
- const addVariableToForm = () => {
- const input = findKeyInputs().at(0);
- input.element.value = 'test_var_2';
- input.trigger('change');
- };
-
beforeEach(() => {
+ // mock is needed when we fully mount
+ // downstream components request needs to be mocked
mock = new MockAdapter(axios);
createComponent(mountExtended);
});
@@ -157,4 +208,229 @@ describe('Pipeline schedules form', () => {
expect(findVariableRows()).toHaveLength(1);
});
});
+
+ describe('Button text', () => {
+ it.each`
+ editing | expectedText
+ ${true} | ${'Edit pipeline schedule'}
+ ${false} | ${'Create pipeline schedule'}
+ `(
+ 'button text is $expectedText when editing is $editing',
+ async ({ editing, expectedText }) => {
+ createComponent(shallowMountExtended, editing, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findSubmitButton().text()).toBe(expectedText);
+ },
+ );
+ });
+
+ describe('Schedule creation', () => {
+ it('when creating a schedule the query is not called', () => {
+ createComponent();
+
+ expect(querySuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not show loading state when creating new schedule', () => {
+ createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('schedule creation success', () => {
+ let mock;
+
+ beforeEach(() => {
+ // mock is needed when we fully mount
+ // downstream components request needs to be mocked
+ mock = new MockAdapter(axios);
+ createComponent(mountExtended);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('creates pipeline schedule', async () => {
+ findDescription().element.value = 'My schedule';
+ findDescription().trigger('change');
+
+ findTimezoneDropdown().vm.$emit('input', {
+ formattedTimezone: '[UTC-4] Eastern Time (US & Canada)',
+ identifier: 'America/New_York',
+ });
+
+ findIntervalComponent().vm.$emit('cronValue', '0 16 * * *');
+
+ addVariableToForm();
+
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(createMutationHandlerSuccess).toHaveBeenCalledWith({
+ input: {
+ active: true,
+ cron: '0 16 * * *',
+ cronTimezone: 'America/New_York',
+ description: 'My schedule',
+ projectPath: 'gitlab-org/gitlab',
+ ref: 'main',
+ variables: [
+ {
+ key: 'test_var_2',
+ value: '',
+ variableType: 'ENV_VAR',
+ },
+ ],
+ },
+ });
+ expect(visitUrl).toHaveBeenCalledWith('/root/ci-project/-/pipeline_schedules');
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('schedule creation failure', () => {
+ beforeEach(() => {
+ createComponent(shallowMountExtended, false, [
+ [createPipelineScheduleMutation, createMutationHandlerFailed],
+ ]);
+ });
+
+ it('shows error for failed pipeline schedule creation', async () => {
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while creating the pipeline schedule.',
+ });
+ });
+ });
+ });
+
+ describe('Schedule editing', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows loading state when editing', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ ]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('schedule fetch success', () => {
+ it('fetches schedule and sets form data correctly', async () => {
+ createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]);
+
+ expect(querySuccessHandler).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findDescription().element.value).toBe(schedule.description);
+ expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron);
+ expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone);
+ expect(findRefSelector().props('value')).toBe(schedule.ref);
+ expect(findVariableRows()).toHaveLength(3);
+ expect(findKeyInputs().at(0).element.value).toBe(variables[0].key);
+ expect(findKeyInputs().at(1).element.value).toBe(variables[1].key);
+ expect(findValueInputs().at(0).element.value).toBe(variables[0].value);
+ expect(findValueInputs().at(1).element.value).toBe(variables[1].value);
+ });
+ });
+
+ it('schedule fetch failure', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, queryFailedHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while trying to fetch the pipeline schedule.',
+ });
+ });
+
+ it('edit schedule success', async () => {
+ createComponent(mountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ [updatePipelineScheduleMutation, updateMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findDescription().element.value = 'Updated schedule';
+ findDescription().trigger('change');
+
+ findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *');
+
+ // Ensures variable is sent with destroy property set true
+ findRemoveIcons().at(0).vm.$emit('click');
+
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({
+ input: {
+ active: schedule.active,
+ cron: '0 22 16 * *',
+ cronTimezone: schedule.cronTimezone,
+ id: schedule.id,
+ ref: schedule.ref,
+ description: 'Updated schedule',
+ variables: [
+ {
+ destroy: true,
+ id: variables[0].id,
+ key: variables[0].key,
+ value: variables[0].value,
+ variableType: variables[0].variableType,
+ },
+ {
+ destroy: false,
+ id: variables[1].id,
+ key: variables[1].key,
+ value: variables[1].value,
+ variableType: variables[1].variableType,
+ },
+ ],
+ },
+ });
+ });
+
+ it('edit schedule failure', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ [updatePipelineScheduleMutation, updateMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while updating the pipeline schedule.',
+ });
+ });
+ });
});
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 50008cedd9c..01a19711264 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -57,6 +57,7 @@ describe('Pipeline schedules app', () => {
wrapper = mountExtended(PipelineSchedules, {
provide: {
fullPath: 'gitlab-org/gitlab',
+ newSchedulePath: '/root/ci-project/-/pipeline_schedules/new',
},
mocks: {
$toast,
@@ -101,6 +102,10 @@ describe('Pipeline schedules app', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
+
+ it('new schedule button links to new schedule path', () => {
+ expect(findNewButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules/new');
+ });
});
describe('fetching pipeline schedules', () => {
@@ -146,15 +151,13 @@ describe('Pipeline schedules app', () => {
[deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
]);
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
-
await waitForPromises();
const scheduleId = mockPipelineScheduleNodes[0].id;
findTable().vm.$emit('showDeleteModal', scheduleId);
- expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(1);
findDeleteModal().vm.$emit('deleteSchedule');
@@ -163,7 +166,7 @@ describe('Pipeline schedules app', () => {
expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
id: scheduleId,
});
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(2);
expect($toast.show).toHaveBeenCalledWith('Pipeline schedule successfully deleted.');
});
@@ -252,15 +255,13 @@ describe('Pipeline schedules app', () => {
[takeOwnershipMutation, takeOwnershipMutationHandlerSuccess],
]);
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
-
await waitForPromises();
const scheduleId = mockPipelineScheduleNodes[1].id;
findTable().vm.$emit('showTakeOwnershipModal', scheduleId);
- expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(1);
findTakeOwnershipModal().vm.$emit('takeOwnership');
@@ -269,7 +270,7 @@ describe('Pipeline schedules app', () => {
expect(takeOwnershipMutationHandlerSuccess).toHaveBeenCalledWith({
id: scheduleId,
});
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(2);
expect($toast.show).toHaveBeenCalledWith('Successfully taken ownership from Admin.');
});
@@ -297,7 +298,7 @@ describe('Pipeline schedules app', () => {
describe('pipeline schedule tabs', () => {
beforeEach(async () => {
- createComponent();
+ createComponent([[getPipelineSchedulesQuery, successHandler]]);
await waitForPromises();
});
@@ -315,13 +316,23 @@ describe('Pipeline schedules app', () => {
});
it('should refetch the schedules query on a tab click', async () => {
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch').mockImplementation(jest.fn());
-
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(0);
+ expect(successHandler).toHaveBeenCalledTimes(1);
await findAllTab().trigger('click');
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(1);
+ expect(successHandler).toHaveBeenCalledTimes(3);
+ });
+
+ it('all tab click should not send scope value with query', async () => {
+ findAllTab().trigger('click');
+
+ await nextTick();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ ids: null,
+ projectPath: 'gitlab-org/gitlab',
+ status: null,
+ });
});
});
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 be0052fc7cf..5eca355fcf4 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
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
mockPipelineScheduleNodes,
mockPipelineScheduleCurrentUser,
@@ -28,6 +29,7 @@ describe('Pipeline schedule actions', () => {
const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn');
+ const findEditScheduleBtn = () => wrapper.findByTestId('edit-pipeline-schedule-btn');
it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => {
createComponent();
@@ -76,4 +78,15 @@ describe('Pipeline schedule actions', () => {
playPipelineSchedule: [[mockPipelineScheduleNodes[0].id]],
});
});
+
+ it('edit button links to edit schedule path', () => {
+ createComponent();
+
+ const { schedule } = defaultProps;
+ const id = getIdFromGraphQLId(schedule.id);
+
+ const expectedPath = `${schedule.editPath}?id=${id}`;
+
+ expect(findEditScheduleBtn().attributes('href')).toBe(expectedPath);
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 1485f6beea4..0a4f233f199 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -2,6 +2,7 @@
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
+import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json';
const {
data: {
@@ -30,15 +31,22 @@ const {
export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleCurrentUser = currentUser;
-
export const mockPipelineScheduleAsGuestNodes = guestNodes;
-
export const mockTakeOwnershipNodes = takeOwnershipNodes;
+export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse;
+
export const emptyPipelineSchedulesResponse = {
data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ username: 'root',
+ },
project: {
id: 'gid://gitlab/Project/1',
- pipelineSchedules: { nodes: [], count: 0 },
+ pipelineSchedules: {
+ count: 0,
+ nodes: [],
+ },
},
},
};
@@ -79,4 +87,24 @@ export const takeOwnershipMutationResponse = {
},
};
+export const createScheduleMutationResponse = {
+ data: {
+ pipelineScheduleCreate: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineScheduleCreatePayload',
+ },
+ },
+};
+
+export const updateScheduleMutationResponse = {
+ data: {
+ pipelineScheduleUpdate: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineScheduleUpdatePayload',
+ },
+ },
+};
+
export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
deleted file mode 100644
index 311a67a3e31..00000000000
--- a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ /dev/null
@@ -1,26 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = `
-Object {
- "length": 4,
- "remain": 20,
- "rtag": "div",
- "size": 32,
- "wclass": "report-block-list",
- "wtag": "ul",
-}
-`;
-
-exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
-Object {
- "component": "CodequalityIssueBody",
- "iconComponent": "IssueStatusIcon",
- "isNew": false,
- "issue": Object {
- "name": "foo",
- },
- "showReportSectionStatusIcon": false,
- "status": "none",
- "statusIconSize": 24,
-}
-`;
diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
deleted file mode 100644
index 8beec220802..00000000000
--- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue';
-import ReportItem from '~/ci/reports/components/report_item.vue';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
-
-describe('Grouped Issues List', () => {
- let wrapper;
-
- const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
- wrapper = shallowMount(GroupedIssuesList, {
- propsData,
- stubs,
- });
- };
-
- const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
-
- it('renders a smart virtual list with the correct props', () => {
- createComponent({
- propsData: {
- resolvedIssues: [{ name: 'foo' }],
- unresolvedIssues: [{ name: 'bar' }],
- },
- stubs: {
- SmartVirtualList,
- },
- });
-
- expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot();
- });
-
- describe('without data', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => {
- expect(findHeading(issueName).exists()).toBe(false);
- });
-
- it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => {
- expect(wrapper.findComponent(ReportItem).exists()).toBe(false);
- });
- });
-
- describe('with data', () => {
- it.each`
- givenIssues | givenHeading | groupName
- ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'}
- ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'}
- `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => {
- createComponent({
- propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading },
- });
-
- expect(findHeading(groupName).text()).toBe(givenHeading);
- });
-
- it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => {
- const issues = [{ name: 'foo' }, { name: 'bar' }];
-
- createComponent({
- propsData: { [`${issueName}Issues`]: issues },
- });
-
- expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length);
- });
-
- it('renders a report item with the correct props', () => {
- createComponent({
- propsData: {
- resolvedIssues: [{ name: 'foo' }],
- component: 'CodequalityIssueBody',
- },
- stubs: {
- ReportItem,
- },
- });
-
- expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
deleted file mode 100644
index b1ae9e26b5b..00000000000
--- a/spec/frontend/ci/reports/components/summary_row_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import SummaryRow from '~/ci/reports/components/summary_row.vue';
-
-describe('Summary row', () => {
- let wrapper;
-
- const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability';
- const popoverOptions = {
- title: 'Static Application Security Testing (SAST)',
- content: '<a>Learn more about SAST</a>',
- };
- const statusIcon = 'warning';
-
- const createComponent = ({ props = {}, slots = {} } = {}) => {
- wrapper = extendedWrapper(
- mount(SummaryRow, {
- propsData: {
- summary,
- popoverOptions,
- statusIcon,
- ...props,
- },
- slots,
- }),
- );
- };
-
- const findSummary = () => wrapper.findByTestId('summary-row-description');
- const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
- const findHelpPopover = () => wrapper.findComponent(HelpPopover);
-
- it('renders provided summary', () => {
- createComponent();
- expect(findSummary().text()).toContain(summary);
- });
-
- it('renders provided icon', () => {
- createComponent();
- expect(findStatusIcon().classes()).toContain('js-ci-status-icon-warning');
- });
-
- it('renders help popover if popoverOptions are provided', () => {
- createComponent();
- expect(findHelpPopover().props('options')).toEqual(popoverOptions);
- });
-
- it('does not render help popover if popoverOptions are not provided', () => {
- createComponent({ props: { popoverOptions: null } });
- expect(findHelpPopover().exists()).toBe(false);
- });
-
- describe('summary slot', () => {
- it('replaces the summary prop', () => {
- const summarySlotContent = 'Summary slot content';
- createComponent({ slots: { summary: summarySlotContent } });
-
- expect(wrapper.text()).not.toContain(summary);
- expect(findSummary().text()).toContain(summarySlotContent);
- });
- });
-});
diff --git a/spec/frontend/ci/reports/mock_data/mock_data.js b/spec/frontend/ci/reports/mock_data/mock_data.js
index 2599b0ac365..2983a9f1125 100644
--- a/spec/frontend/ci/reports/mock_data/mock_data.js
+++ b/spec/frontend/ci/reports/mock_data/mock_data.js
@@ -1,3 +1,6 @@
+import { SEVERITIES as SEVERITIES_CODE_QUALITY } from '~/ci/reports/codequality_report/constants';
+import { SEVERITIES as SEVERITIES_SAST } from '~/ci/reports/sast/constants';
+
export const failedIssue = {
result: 'failure',
name: 'Test#sum when a is 1 and b is 2 returns summary',
@@ -36,3 +39,54 @@ export const failedReport = {
},
],
};
+
+export const findingSastInfo = {
+ scale: 'sast',
+ severity: 'info',
+};
+
+export const findingSastInfoEnhanced = {
+ scale: 'sast',
+ severity: 'info',
+ class: SEVERITIES_SAST.info.class,
+ name: SEVERITIES_SAST.info.name,
+};
+
+export const findingsCodeQualityBlocker = {
+ scale: 'codeQuality',
+ severity: 'blocker',
+};
+
+export const findingCodeQualityBlockerEnhanced = {
+ scale: 'codeQuality',
+ severity: 'blocker',
+ class: SEVERITIES_CODE_QUALITY.blocker.class,
+ name: SEVERITIES_CODE_QUALITY.blocker.name,
+};
+
+export const findingCodeQualityInfo = {
+ scale: 'codeQuality',
+ severity: 'info',
+};
+
+export const findingCodeQualityInfoEnhanced = {
+ scale: 'codeQuality',
+ severity: 'info',
+ class: SEVERITIES_CODE_QUALITY.info.class,
+ name: SEVERITIES_CODE_QUALITY.info.name,
+};
+
+export const findingUnknownInfo = {
+ scale: 'codeQuality',
+ severity: 'info',
+};
+
+export const findingUnknownInfoEnhanced = {
+ scale: 'codeQuality',
+ severity: 'info',
+ class: SEVERITIES_CODE_QUALITY.info.class,
+ name: SEVERITIES_CODE_QUALITY.info.name,
+};
+
+export const findingsArray = [findingSastInfo, findingsCodeQualityBlocker];
+export const findingsArrayEnhanced = [findingSastInfoEnhanced, findingCodeQualityBlockerEnhanced];
diff --git a/spec/frontend/ci/reports/utils_spec.js b/spec/frontend/ci/reports/utils_spec.js
new file mode 100644
index 00000000000..e01aa903a97
--- /dev/null
+++ b/spec/frontend/ci/reports/utils_spec.js
@@ -0,0 +1,30 @@
+import { getSeverity } from '~/ci/reports/utils';
+
+import {
+ findingSastInfo,
+ findingSastInfoEnhanced,
+ findingCodeQualityInfo,
+ findingCodeQualityInfoEnhanced,
+ findingUnknownInfo,
+ findingUnknownInfoEnhanced,
+ findingsArray,
+ findingsArrayEnhanced,
+} from './mock_data/mock_data';
+
+describe('getSeverity utility function', () => {
+ it('should enhance finding with sast scale', () => {
+ expect(getSeverity(findingSastInfo)).toEqual(findingSastInfoEnhanced);
+ });
+
+ it('should enhance finding with codequality scale', () => {
+ expect(getSeverity(findingCodeQualityInfo)).toEqual(findingCodeQualityInfoEnhanced);
+ });
+
+ it('should use codeQuality scale when scale is unknown', () => {
+ expect(getSeverity(findingUnknownInfo)).toEqual(findingUnknownInfoEnhanced);
+ });
+
+ it('should correctly enhance an array of findings', () => {
+ expect(getSeverity(findingsArray)).toEqual(findingsArrayEnhanced);
+ });
+});
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 c4ed6d1bdb5..c9349c64bfb 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
@@ -9,10 +9,8 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
import RunnerDetails from '~/ci/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
@@ -46,9 +44,7 @@ describe('AdminRunnerShowApp', () => {
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
- const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
- const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
- const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerHeaderActions = () => wrapper.findComponent(RunnerHeaderActions);
const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
@@ -94,9 +90,10 @@ describe('AdminRunnerShowApp', () => {
});
it('displays the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
- expect(findRunnerPauseButton().exists()).toBe(true);
- expect(findRunnerDeleteButton().exists()).toBe(true);
+ expect(findRunnerHeaderActions().props()).toEqual({
+ runner: mockRunner,
+ editPath: mockRunner.editAdminUrl,
+ });
});
it('shows runner details', () => {
@@ -122,54 +119,6 @@ describe('AdminRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- describe('when runner cannot be updated', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- updateRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(false);
- expect(findRunnerPauseButton().exists()).toBe(false);
- });
-
- it('displays delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(true);
- });
- });
-
- describe('when runner cannot be deleted', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- deleteRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(false);
- });
-
- it('displays edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(true);
- expect(findRunnerPauseButton().exists()).toBe(true);
- });
- });
-
describe('when runner is deleted', () => {
beforeEach(async () => {
await createComponent({
@@ -178,7 +127,7 @@ describe('AdminRunnerShowApp', () => {
});
it('redirects to the runner list page', () => {
- findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerHeaderActions().vm.$emit('deleted', { message: 'Runner deleted' });
expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
message: 'Runner deleted',
@@ -187,23 +136,6 @@ describe('AdminRunnerShowApp', () => {
expect(visitUrl).toHaveBeenCalledWith(mockRunnersPath);
});
});
-
- describe('when runner does not have an edit url', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- editAdminUrl: null,
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the runner edit button', () => {
- expect(findRunnerEditButton().exists()).toBe(false);
- expect(findRunnerPauseButton().exists()).toBe(true);
- });
- });
});
describe('When loading', () => {
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 fc74e2947b6..1bbcb991619 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
@@ -156,7 +156,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/414975
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/414975
// eslint-disable-next-line jest/no-disabled-tests
it.skip('fetches counts', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
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 cda3876f9b2..ad20d7682ed 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
@@ -1,5 +1,6 @@
+import { GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -31,8 +32,8 @@ describe('RunnerTypeCell', () => {
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
.wrappers[0];
- const createComponent = (runner, options) => {
- wrapper = mountExtended(RunnerSummaryCell, {
+ const createComponent = ({ runner, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerSummaryCell, {
propsData: {
runner: {
...mockRunner,
@@ -40,7 +41,7 @@ describe('RunnerTypeCell', () => {
},
},
stubs: {
- RunnerSummaryField,
+ GlSprintf,
},
...options,
});
@@ -51,6 +52,8 @@ describe('RunnerTypeCell', () => {
});
it('Displays the runner name as id and short token', () => {
+ createComponent({ mountFn: mountExtended });
+
expect(wrapper.text()).toContain(
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
);
@@ -58,13 +61,16 @@ describe('RunnerTypeCell', () => {
it('Displays no runner manager count', () => {
createComponent({
- managers: { count: 0 },
+ runner: { managers: { nodes: { count: 0 } } },
+ mountFn: mountExtended,
});
expect(findRunnerManagersBadge().html()).toBe('');
});
it('Displays runner manager count', () => {
+ createComponent({ mountFn: mountExtended });
+
expect(findRunnerManagersBadge().text()).toBe('2');
});
@@ -74,8 +80,8 @@ describe('RunnerTypeCell', () => {
it('Displays the locked icon for locked runners', () => {
createComponent({
- runnerType: PROJECT_TYPE,
- locked: true,
+ runner: { runnerType: PROJECT_TYPE, locked: true },
+ mountFn: mountExtended,
});
expect(findLockIcon().exists()).toBe(true);
@@ -83,8 +89,8 @@ describe('RunnerTypeCell', () => {
it('Displays the runner type', () => {
createComponent({
- runnerType: INSTANCE_TYPE,
- locked: true,
+ runner: { runnerType: INSTANCE_TYPE, locked: true },
+ mountFn: mountExtended,
});
expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE);
@@ -101,7 +107,7 @@ describe('RunnerTypeCell', () => {
it('Displays "No description" for missing runner description', () => {
createComponent({
- description: null,
+ runner: { description: null },
});
expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
@@ -109,7 +115,7 @@ describe('RunnerTypeCell', () => {
it('Displays last contact', () => {
createComponent({
- contactedAt: '2022-01-02',
+ runner: { contactedAt: '2022-01-02' },
});
expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02');
@@ -124,20 +130,46 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('clock').text()).toContain(__('Never'));
});
- it('Displays ip address', () => {
- createComponent({
- ipAddress: '127.0.0.1',
+ describe('IP address', () => {
+ it('with no managers', () => {
+ createComponent({
+ runner: {
+ managers: { count: 0, nodes: [] },
+ },
+ });
+
+ expect(findRunnerSummaryField('disk')).toBeUndefined();
});
- expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1');
- });
+ it('with no ip', () => {
+ createComponent({
+ runner: {
+ managers: { count: 1, nodes: [{ ipAddress: null }] },
+ },
+ });
- it('Displays no ip address', () => {
- createComponent({
- ipAddress: null,
+ expect(findRunnerSummaryField('disk')).toBeUndefined();
});
- expect(findRunnerSummaryField('disk')).toBeUndefined();
+ it.each`
+ count | ipAddress | expected
+ ${1} | ${'127.0.0.1'} | ${'127.0.0.1'}
+ ${2} | ${'127.0.0.2'} | ${'127.0.0.2 (+1)'}
+ ${11} | ${'127.0.0.3'} | ${'127.0.0.3 (+10)'}
+ ${1001} | ${'127.0.0.4'} | ${'127.0.0.4 (+1,000)'}
+ `(
+ 'with $count managers, ip $ipAddress displays $expected',
+ ({ count, ipAddress, expected }) => {
+ createComponent({
+ runner: {
+ // `first: 1` is requested, `count` varies when there are more managers
+ managers: { count, nodes: [{ ipAddress }] },
+ },
+ });
+
+ expect(findRunnerSummaryField('disk').text()).toMatchInterpolatedText(expected);
+ },
+ );
});
it('Displays job count', () => {
@@ -146,7 +178,7 @@ describe('RunnerTypeCell', () => {
it('Formats large job counts', () => {
createComponent({
- jobCount: 1000,
+ runner: { jobCount: 1000 },
});
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000');
@@ -154,7 +186,7 @@ describe('RunnerTypeCell', () => {
it('Formats large job counts with a plus symbol', () => {
createComponent({
- jobCount: 1001,
+ runner: { jobCount: 1001 },
});
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
@@ -165,7 +197,7 @@ describe('RunnerTypeCell', () => {
it('Displays created at ...', () => {
createComponent({
- createdBy: null,
+ runner: { createdBy: null },
});
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
@@ -177,12 +209,15 @@ describe('RunnerTypeCell', () => {
});
it('Displays created at ... by ...', () => {
+ createComponent({ mountFn: mountExtended });
+
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
sprintf(I18N_CREATED_AT_BY_LABEL, {
timeAgo: findCreatedTime().text(),
avatar: mockRunner.createdBy.username,
}),
);
+
expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
});
@@ -200,7 +235,7 @@ describe('RunnerTypeCell', () => {
it('Displays tag list', () => {
createComponent({
- tagList: ['shell', 'linux'],
+ runner: { tagList: ['shell', 'linux'] },
});
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
@@ -209,14 +244,11 @@ describe('RunnerTypeCell', () => {
it('Displays a custom runner-name slot', () => {
const slotContent = 'My custom runner name';
- createComponent(
- {},
- {
- slots: {
- 'runner-name': slotContent,
- },
+ createComponent({
+ slots: {
+ 'runner-name': slotContent,
},
- );
+ });
expect(wrapper.text()).toContain(slotContent);
});
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 e564cf49ca0..e4373d1c198 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,4 +1,10 @@
-import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm, GlIcon } from '@gitlab/ui';
+import {
+ GlModal,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDropdownForm,
+ GlIcon,
+} from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -35,13 +41,16 @@ Vue.use(VueApollo);
describe('RegistrationDropdown', () => {
let wrapper;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownBtn = () => findDropdown().find('button');
- const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findRegistrationInstructionsDropdownItem = () =>
+ wrapper.findComponent(GlDisclosureDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
const findRegistrationTokenInput = () =>
- wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
+ wrapper.findByLabelText(
+ `${RegistrationToken.i18n.registrationToken} ${RegistrationDropdown.i18n.supportForRegistrationTokensDeprecated}`,
+ );
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
const findModal = () => wrapper.findComponent(GlModal);
@@ -52,9 +61,8 @@ describe('RegistrationDropdown', () => {
.replace(/[\n\t\s]+/g, ' ');
const openModal = async () => {
- await findRegistrationInstructionsDropdownItem().trigger('click');
+ await findRegistrationInstructionsDropdownItem().vm.$emit('action');
findModal().vm.$emit('shown');
-
await waitForPromises();
};
@@ -65,6 +73,9 @@ describe('RegistrationDropdown', () => {
type: INSTANCE_TYPE,
...props,
},
+ stubs: {
+ GlDisclosureDropdownItem,
+ },
...options,
});
};
@@ -107,12 +118,12 @@ describe('RegistrationDropdown', () => {
createComponent();
expect(findDropdown().props()).toMatchObject({
- category: 'primary',
- variant: 'confirm',
+ category: 'tertiary',
+ variant: 'default',
});
expect(findDropdown().attributes()).toMatchObject({
- toggleclass: '',
+ toggleclass: 'gl-px-3!',
});
});
@@ -186,6 +197,26 @@ describe('RegistrationDropdown', () => {
});
});
+ describe('Dropdown is expanded', () => {
+ beforeEach(() => {
+ createComponent({}, mountExtended);
+ findDropdownBtn().vm.$emit('click');
+ });
+
+ it('has aria-expanded set to true', () => {
+ expect(findDropdownBtn().attributes('aria-expanded')).toBe('true');
+ });
+
+ describe('when token is copied', () => {
+ it('should close dropdown', async () => {
+ findRegistrationToken().vm.$emit('copy');
+ await nextTick();
+
+ expect(findDropdownBtn().attributes('aria-expanded')).toBeUndefined();
+ });
+ });
+ });
+
describe('When token is reset', () => {
const newToken = 'mock1';
@@ -217,19 +248,15 @@ describe('RegistrationDropdown', () => {
});
});
- describe.each([
- { createRunnerWorkflowForAdmin: true },
- { createRunnerWorkflowForNamespace: true },
- ])('When showing a "deprecated" warning', (glFeatures) => {
+ describe('When showing a "deprecated" warning', () => {
it('passes deprecated variant props and attributes to dropdown', () => {
- createComponent({
- provide: { glFeatures },
- });
+ createComponent();
expect(findDropdown().props()).toMatchObject({
category: 'tertiary',
variant: 'default',
- text: '',
+ toggleText: I18N_REGISTER_INSTANCE_TYPE,
+ textSrOnly: true,
});
expect(findDropdown().attributes()).toMatchObject({
@@ -249,12 +276,7 @@ describe('RegistrationDropdown', () => {
});
it('shows warning text', () => {
- createComponent(
- {
- provide: { glFeatures },
- },
- mountExtended,
- );
+ createComponent({}, mountExtended);
const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
@@ -262,12 +284,7 @@ describe('RegistrationDropdown', () => {
});
it('button shows ellipsis icon', () => {
- createComponent(
- {
- provide: { glFeatures },
- },
- mountExtended,
- );
+ createComponent({}, mountExtended);
expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
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 db54bf0c80e..d599bc1291c 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
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -27,7 +27,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
let showToast;
const mockEvent = { preventDefault: jest.fn() };
- const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findModal = () => wrapper.findComponent(GlModal);
const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
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 869c032c0b5..fd3896d5500 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -7,7 +7,7 @@ import { mockRegistrationToken } from '../../mock_data';
describe('RegistrationToken', () => {
let wrapper;
- let showToast;
+ const showToastMock = jest.fn();
Vue.use(GlToast);
@@ -21,9 +21,12 @@ describe('RegistrationToken', () => {
...props,
},
...options,
+ mocks: {
+ $toast: {
+ show: showToastMock,
+ },
+ },
});
-
- showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
it('Displays value and copy button', () => {
@@ -58,8 +61,14 @@ describe('RegistrationToken', () => {
it('shows a copied message', () => {
findInputCopyToggleVisibility().vm.$emit('copy');
- expect(showToast).toHaveBeenCalledTimes(1);
- expect(showToast).toHaveBeenCalledWith('Registration token copied!');
+ expect(showToastMock).toHaveBeenCalledTimes(1);
+ expect(showToastMock).toHaveBeenCalledWith('Registration token copied!');
+ });
+
+ it('emits a copy event', () => {
+ findInputCopyToggleVisibility().vm.$emit('copy');
+
+ expect(wrapper.emitted('copy')).toHaveLength(1);
});
});
@@ -76,9 +85,7 @@ describe('RegistrationToken', () => {
});
it('passes slots to the input component', () => {
- const slot = findInputCopyToggleVisibility().vm.$scopedSlots[slotName];
-
- expect(slot()[0].text).toBe(slotContent);
+ expect(findInputCopyToggleVisibility().text()).toBe(slotContent);
});
});
});
diff --git a/spec/frontend/ci/runner/components/runner_delete_action_spec.js b/spec/frontend/ci/runner/components/runner_delete_action_spec.js
new file mode 100644
index 00000000000..d6617e6e75c
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_delete_action_spec.js
@@ -0,0 +1,223 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createAlert } from '~/alert';
+
+import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue';
+import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
+import { allRunnersData } from '../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
+const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+describe('RunnerDeleteAction', () => {
+ let wrapper;
+ let apolloProvider;
+ let apolloCache;
+ let runnerDeleteHandler;
+ let mockModalShow;
+
+ const findBtn = () => wrapper.find('button');
+ const findModal = () => wrapper.findComponent(RunnerDeleteModal);
+
+ const createComponent = ({ props = {} } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = shallowMountExtended(RunnerDeleteAction, {
+ propsData: {
+ runner: {
+ // We need typename so that cache.identify works
+ // eslint-disable-next-line no-underscore-dangle
+ __typename: mockRunner.__typename,
+ id: mockRunner.id,
+ shortSha: mockRunner.shortSha,
+ ...runner,
+ },
+ ...propsData,
+ },
+ apolloProvider,
+ stubs: {
+ RunnerDeleteModal: stubComponent(RunnerDeleteModal, {
+ methods: {
+ show: mockModalShow,
+ },
+ }),
+ },
+ scopedSlots: {
+ default: '<button :disabled="props.loading" @click="props.onClick"/>',
+ },
+ });
+ };
+
+ const clickOkAndWait = async () => {
+ findModal().vm.$emit('primary');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mockModalShow = jest.fn();
+
+ runnerDeleteHandler = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ runnerDelete: {
+ errors: [],
+ },
+ },
+ });
+ });
+ apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]);
+ apolloCache = apolloProvider.defaultClient.cache;
+
+ jest.spyOn(apolloCache, 'evict');
+ jest.spyOn(apolloCache, 'gc');
+
+ createComponent();
+ });
+
+ it('Displays an action in the slot', () => {
+ expect(findBtn().exists()).toBe(true);
+ });
+
+ it('Displays a modal with the runner name', () => {
+ expect(findModal().props('runnerName')).toBe(mockRunnerName);
+ });
+
+ it('Displays a modal with the runner manager count', () => {
+ createComponent({
+ props: {
+ runner: { managers: { count: 2 } },
+ },
+ });
+
+ expect(findModal().props('managersCount')).toBe(2);
+ });
+
+ it('Displays a modal when action is triggered', async () => {
+ await findBtn().trigger('click');
+
+ expect(mockModalShow).toHaveBeenCalled();
+ });
+
+ describe('Before the delete button is clicked', () => {
+ it('The mutation has not been called', () => {
+ expect(runnerDeleteHandler).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('Immediately after the delete button is clicked', () => {
+ beforeEach(() => {
+ findModal().vm.$emit('primary');
+ });
+
+ it('The button has a loading state', () => {
+ expect(findBtn().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('After clicking on the delete button', () => {
+ beforeEach(async () => {
+ await clickOkAndWait();
+ });
+
+ it('The mutation to delete is called', () => {
+ expect(runnerDeleteHandler).toHaveBeenCalledTimes(1);
+ expect(runnerDeleteHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ },
+ });
+ });
+
+ it('The user can be notified with an event', () => {
+ const done = wrapper.emitted('done');
+
+ expect(done).toHaveLength(1);
+ expect(done[0][0].message).toMatch(`#${mockRunnerId}`);
+ expect(done[0][0].message).toMatch(`${mockRunner.shortSha}`);
+ });
+
+ it('evicts runner from apollo cache', () => {
+ expect(apolloCache.evict).toHaveBeenCalledWith({
+ id: apolloCache.identify(mockRunner),
+ });
+ expect(apolloCache.gc).toHaveBeenCalled();
+ });
+ });
+
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerDeleteHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await clickOkAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(mockErrorMsg),
+ component: 'RunnerDeleteAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: mockErrorMsg,
+ });
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerDeleteHandler.mockResolvedValueOnce({
+ data: {
+ runnerDelete: {
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await clickOkAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerDeleteAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: `${mockErrorMsg} ${mockErrorMsg2}`,
+ });
+ });
+
+ it('does not evict runner from apollo cache', () => {
+ expect(apolloCache.evict).not.toHaveBeenCalled();
+ expect(apolloCache.gc).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 3b3f3b1770d..87e857510de 100644
--- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -1,110 +1,73 @@
-import Vue from 'vue';
import { GlButton } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
-import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/ci/runner/sentry_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
-import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
+import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue';
import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
-const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
-const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
-
-Vue.use(VueApollo);
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
- let apolloProvider;
- let apolloCache;
- let runnerDeleteHandler;
const findBtn = () => wrapper.findComponent(GlButton);
- const findModal = () => wrapper.findComponent(RunnerDeleteModal);
-
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const getModal = () => getBinding(findBtn().element, 'gl-modal').value;
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
- const { runner, ...propsData } = props;
-
- wrapper = mountFn(RunnerDeleteButton, {
+ const createComponent = ({ props = {}, loading, onClick = jest.fn() } = {}) => {
+ wrapper = shallowMountExtended(RunnerDeleteButton, {
propsData: {
- runner: {
- // We need typename so that cache.identify works
- // eslint-disable-next-line no-underscore-dangle
- __typename: mockRunner.__typename,
- id: mockRunner.id,
- shortSha: mockRunner.shortSha,
- ...runner,
- },
- ...propsData,
+ runner: mockRunner,
+ ...props,
},
- apolloProvider,
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
- GlModal: createMockDirective('gl-modal'),
+ },
+ stubs: {
+ RunnerDeleteAction: stubComponent(RunnerDeleteAction, {
+ render() {
+ return this.$scopedSlots.default({
+ loading,
+ onClick,
+ });
+ },
+ }),
},
});
};
- const clickOkAndWait = async () => {
- findModal().vm.$emit('primary');
- await waitForPromises();
- };
-
beforeEach(() => {
- runnerDeleteHandler = jest.fn().mockImplementation(() => {
- return Promise.resolve({
- data: {
- runnerDelete: {
- errors: [],
- },
- },
- });
- });
- apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]);
- apolloCache = apolloProvider.defaultClient.cache;
-
- jest.spyOn(apolloCache, 'evict');
- jest.spyOn(apolloCache, 'gc');
-
createComponent();
});
- it('Displays a delete button without an icon', () => {
+ it('Displays a delete button without a icon or tooltip', () => {
expect(findBtn().props()).toMatchObject({
loading: false,
icon: '',
});
expect(findBtn().classes('btn-icon')).toBe(false);
expect(findBtn().text()).toBe(I18N_DELETE_RUNNER);
- });
- it('Displays a modal with the runner name', () => {
- expect(findModal().props('runnerName')).toBe(mockRunnerName);
+ expect(getTooltip()).toBe('');
});
it('Does not have tabindex when button is enabled', () => {
expect(wrapper.attributes('tabindex')).toBeUndefined();
});
- it('Displays a modal when clicked', () => {
- const modalId = `delete-runner-modal-${mockRunnerId}`;
+ it('Triggers delete when clicked', () => {
+ const mockOnClick = jest.fn();
+
+ createComponent({ onClick: mockOnClick });
+ expect(mockOnClick).not.toHaveBeenCalled();
- expect(getModal()).toBe(modalId);
- expect(findModal().attributes('modal-id')).toBe(modalId);
+ findBtn().vm.$emit('click');
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('Does not display redundant text for screen readers', () => {
@@ -117,135 +80,41 @@ describe('RunnerDeleteButton', () => {
expect(findBtn().props('category')).toBe('secondary');
});
- describe(`Before the delete button is clicked`, () => {
- it('The mutation has not been called', () => {
- expect(runnerDeleteHandler).toHaveBeenCalledTimes(0);
- });
- });
-
- describe('Immediately after the delete button is clicked', () => {
+ describe('When loading result', () => {
beforeEach(() => {
- findModal().vm.$emit('primary');
+ createComponent({ loading: true });
});
it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
-
- it('The stale tooltip is removed', () => {
- expect(getTooltip()).toBe('');
- });
});
- describe('After clicking on the delete button', () => {
- beforeEach(async () => {
- await clickOkAndWait();
- });
-
- it('The mutation to delete is called', () => {
- expect(runnerDeleteHandler).toHaveBeenCalledTimes(1);
- expect(runnerDeleteHandler).toHaveBeenCalledWith({
- input: {
- id: mockRunner.id,
- },
- });
- });
-
- it('The user can be notified with an event', () => {
- const deleted = wrapper.emitted('deleted');
-
- expect(deleted).toHaveLength(1);
- expect(deleted[0][0].message).toMatch(`#${mockRunnerId}`);
- expect(deleted[0][0].message).toMatch(`${mockRunner.shortSha}`);
- });
-
- it('evicts runner from apollo cache', () => {
- expect(apolloCache.evict).toHaveBeenCalledWith({
- id: apolloCache.identify(mockRunner),
- });
- expect(apolloCache.gc).toHaveBeenCalled();
- });
- });
+ describe('When done after deleting', () => {
+ const doneEvent = { message: 'done!' };
- describe('When update fails', () => {
- describe('On a network error', () => {
- const mockErrorMsg = 'Update error!';
-
- beforeEach(async () => {
- runnerDeleteHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
-
- await clickOkAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(mockErrorMsg),
- component: 'RunnerDeleteButton',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- title: expect.stringContaining(mockRunnerName),
- message: mockErrorMsg,
- });
- });
+ beforeEach(() => {
+ wrapper.findComponent(RunnerDeleteAction).vm.$emit('done', doneEvent);
});
- describe('On a validation error', () => {
- const mockErrorMsg = 'Runner not found!';
- const mockErrorMsg2 = 'User not allowed!';
-
- beforeEach(async () => {
- runnerDeleteHandler.mockResolvedValueOnce({
- data: {
- runnerDelete: {
- errors: [mockErrorMsg, mockErrorMsg2],
- },
- },
- });
-
- await clickOkAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
- component: 'RunnerDeleteButton',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- title: expect.stringContaining(mockRunnerName),
- message: `${mockErrorMsg} ${mockErrorMsg2}`,
- });
- });
-
- it('does not evict runner from apollo cache', () => {
- expect(apolloCache.evict).not.toHaveBeenCalled();
- expect(apolloCache.gc).not.toHaveBeenCalled();
- });
+ it('emits deleted event', () => {
+ expect(wrapper.emitted('deleted')).toEqual([[doneEvent]]);
});
});
- describe('When displaying a compact button for an active runner', () => {
+ describe('When displaying a compact button', () => {
beforeEach(() => {
createComponent({
- props: {
- runner: {
- paused: false,
- },
- compact: true,
- },
- mountFn: mountExtended,
+ props: { compact: true },
});
});
it('Displays no text', () => {
expect(findBtn().text()).toBe('');
+ });
+
+ it('Displays "x" icon', () => {
+ expect(findBtn().props('icon')).toBe('close');
expect(findBtn().classes('btn-icon')).toBe(true);
});
@@ -254,13 +123,12 @@ describe('RunnerDeleteButton', () => {
expect(getTooltip()).toBe(I18N_DELETE_RUNNER);
});
- describe('Immediately after the button is clicked', () => {
+ describe('When loading result', () => {
beforeEach(() => {
- findModal().vm.$emit('primary');
- });
-
- it('The button has a loading state', () => {
- expect(findBtn().props('loading')).toBe(true);
+ createComponent({
+ props: { compact: true },
+ loading: true,
+ });
});
it('The stale tooltip is removed', () => {
diff --git a/spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js b/spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js
new file mode 100644
index 00000000000..e311cb4d458
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js
@@ -0,0 +1,68 @@
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { I18N_DELETE } from '~/ci/runner/constants';
+
+import RunnerDeleteDisclosureDropdownItem from '~/ci/runner/components/runner_delete_disclosure_dropdown_item.vue';
+import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue';
+import { allRunnersData } from '../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+describe('RunnerDeleteDisclosureDropdownItem', () => {
+ let wrapper;
+ let mockOnClick;
+
+ const findDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ const createComponent = () => {
+ mockOnClick = jest.fn();
+
+ wrapper = shallowMountExtended(RunnerDeleteDisclosureDropdownItem, {
+ propsData: {
+ runner: mockRunner,
+ },
+ stubs: {
+ RunnerDeleteAction: stubComponent(RunnerDeleteAction, {
+ render() {
+ return this.$scopedSlots.default({
+ onClick: mockOnClick,
+ });
+ },
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Displays a delete item', () => {
+ expect(findDisclosureDropdownItem().text()).toBe(I18N_DELETE);
+ });
+
+ it('Does not trigger on load', () => {
+ expect(mockOnClick).not.toHaveBeenCalled();
+ });
+
+ it('Triggers delete when clicked', () => {
+ findDisclosureDropdownItem().vm.$emit('action');
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ describe('When done after deleting', () => {
+ const doneEvent = { message: 'done!' };
+
+ beforeEach(() => {
+ wrapper.findComponent(RunnerDeleteAction).vm.$emit('done', doneEvent);
+ });
+
+ it('emits deleted event', () => {
+ expect(wrapper.emitted('deleted')).toEqual([[doneEvent]]);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_delete_modal_spec.js b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
index 606cc46c018..e486d708fec 100644
--- a/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
describe('RunnerDeleteModal', () => {
@@ -7,7 +8,7 @@ describe('RunnerDeleteModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
wrapper = mountFn(RunnerDeleteModal, {
attachTo: document.body,
propsData: {
@@ -17,6 +18,7 @@ describe('RunnerDeleteModal', () => {
attrs: {
modalId: 'delete-runner-modal-99',
},
+ ...options,
});
};
@@ -66,15 +68,35 @@ describe('RunnerDeleteModal', () => {
});
});
- describe('When modal is confirmed by the user', () => {
+ describe('Modal API', () => {
let hideModalSpy;
+ let showModalSpy;
beforeEach(() => {
- createComponent({}, mount);
- hideModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide').mockImplementation(() => {});
+ hideModalSpy = jest.fn();
+ showModalSpy = jest.fn();
+
+ createComponent({
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: hideModalSpy,
+ show: showModalSpy,
+ },
+ }),
+ },
+ });
+ });
+
+ it('When "show" method is called, modal is shown', () => {
+ expect(showModalSpy).toHaveBeenCalledTimes(0);
+
+ wrapper.vm.show();
+
+ expect(showModalSpy).toHaveBeenCalledTimes(1);
});
- it('Modal gets hidden', () => {
+ it('When confirmed, modal gets hidden', () => {
expect(hideModalSpy).toHaveBeenCalledTimes(0);
findGlModal().vm.$emit('primary');
diff --git a/spec/frontend/ci/runner/components/runner_detail_spec.js b/spec/frontend/ci/runner/components/runner_detail_spec.js
new file mode 100644
index 00000000000..b2d91af4e3b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_detail_spec.js
@@ -0,0 +1,88 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerDetail from '~/ci/runner/components/runner_detail.vue';
+
+describe('RunnerDetail', () => {
+ let wrapper;
+ const createWrapper = ({ props, slots }) => {
+ wrapper = shallowMountExtended(RunnerDetail, {
+ propsData: props,
+ slots,
+ });
+ };
+ const findLabelText = () => wrapper.findByTestId('label-slot').text();
+ const findValueText = () => wrapper.findByTestId('value-slot').text();
+
+ it('renders the label slot when a label prop is provided', () => {
+ createWrapper({ props: { label: 'Field Name' } });
+
+ expect(findLabelText()).toBe('Field Name');
+ });
+
+ it('does not render the label slot when no label prop is provided', () => {
+ createWrapper({ props: {} });
+
+ expect(findLabelText()).toBe('');
+ });
+
+ it('renders the value slot when a value prop is provided', () => {
+ createWrapper({ props: { value: 'testValue' } });
+
+ expect(findValueText()).toBe('testValue');
+ });
+
+ it('renders the emptyValue when no value prop is provided', () => {
+ createWrapper({ props: {} });
+
+ expect(findValueText()).toBe('None');
+ });
+
+ it('renders both the label and value slots when both label and value props are provided', () => {
+ createWrapper({ props: { label: 'Field Name', value: 'testValue' } });
+
+ expect(findLabelText()).toBe('Field Name');
+ expect(findValueText()).toBe('testValue');
+ });
+
+ it('renders the label slot when a label slot is provided', () => {
+ createWrapper({
+ slots: {
+ label: 'Label Slot Test',
+ },
+ });
+
+ expect(findLabelText()).toBe('Label Slot Test');
+ });
+
+ it('does not render the label slot when no label slot is provided', () => {
+ createWrapper({
+ slots: {},
+ });
+
+ expect(findLabelText()).toBe('');
+ });
+
+ it('renders the value slot when a value slot is provided', () => {
+ createWrapper({
+ slots: {
+ value: 'Value Slot Test',
+ },
+ });
+
+ expect(findValueText()).toBe('Value Slot Test');
+ });
+
+ it('renders the emptyValue when no value slot is provided', () => {
+ createWrapper({
+ slots: {},
+ });
+
+ expect(findValueText()).toBe('None');
+ });
+
+ it('renders both the label and value slots when both label and value slots are provided', () => {
+ createWrapper({ slots: { label: 'Label Slot Test', value: 'Value Slot Test' } });
+
+ expect(findLabelText()).toBe('Label Slot Test');
+ expect(findValueText()).toBe('Value Slot Test');
+ });
+});
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 5cc1ee049f4..5e36ff77146 100644
--- a/spec/frontend/ci/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -1,18 +1,25 @@
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { I18N_EDIT } from '~/ci/runner/constants';
describe('RunnerEditButton', () => {
let wrapper;
+ const findButton = () => wrapper.findComponent(GlButton);
const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const createComponent = ({ attrs = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerEditButton, {
- attrs,
+ propsData: {
+ href: '/edit',
+ ...props,
+ },
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
+ ...options,
});
};
@@ -21,17 +28,24 @@ describe('RunnerEditButton', () => {
});
it('Displays Edit text', () => {
- expect(wrapper.attributes('aria-label')).toBe('Edit');
+ expect(wrapper.attributes('aria-label')).toBe(I18N_EDIT);
});
it('Displays Edit tooltip', () => {
- expect(getTooltipValue()).toBe('Edit');
+ expect(getTooltipValue()).toBe(I18N_EDIT);
});
it('Renders a link and adds an href attribute', () => {
- createComponent({ attrs: { href: '/edit' }, mountFn: mount });
+ expect(findButton().attributes('href')).toBe('/edit');
+ });
- expect(wrapper.element.tagName).toBe('A');
- expect(wrapper.attributes('href')).toBe('/edit');
+ describe('When no href is provided', () => {
+ beforeEach(() => {
+ createComponent({ props: { href: null } });
+ });
+
+ it('does not render', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
});
diff --git a/spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js b/spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js
new file mode 100644
index 00000000000..4c6b4d2d52a
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import RunnerEditDisclosureDropdownItem from '~/ci/runner/components/runner_edit_disclosure_dropdown_item.vue';
+import { I18N_EDIT } from '~/ci/runner/constants';
+
+describe('RunnerEditDisclosureDropdownItem', () => {
+ let wrapper;
+
+ const findItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
+ wrapper = mountFn(RunnerEditDisclosureDropdownItem, {
+ propsData: {
+ href: '/edit',
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ it('Displays Edit text', () => {
+ createComponent({ mountFn: mount });
+
+ expect(wrapper.text()).toBe(I18N_EDIT);
+ });
+
+ it('Renders a link and adds an href attribute', () => {
+ createComponent();
+
+ expect(findItem().props('item').href).toBe('/edit');
+ });
+
+ describe('When no href is provided', () => {
+ beforeEach(() => {
+ createComponent({ props: { href: null } });
+ });
+
+ it('does not render', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_header_actions_spec.js b/spec/frontend/ci/runner/components/runner_header_actions_spec.js
new file mode 100644
index 00000000000..243ada73435
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_header_actions_spec.js
@@ -0,0 +1,147 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
+
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+
+import RunnerEditDisclosureDropdownItem from '~/ci/runner/components/runner_edit_disclosure_dropdown_item.vue';
+import RunnerPauseDisclosureDropdownItem from '~/ci/runner/components/runner_pause_disclosure_dropdown_item.vue';
+import RunnerDeleteDisclosureDropdownItem from '~/ci/runner/components/runner_delete_disclosure_dropdown_item.vue';
+
+import { runnerData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerEditPath = '/edit';
+
+describe('RunnerHeaderActions', () => {
+ let wrapper;
+
+ const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
+ const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findEditItem = () => findDropdown().findComponent(RunnerEditDisclosureDropdownItem);
+ const findPauseItem = () => findDropdown().findComponent(RunnerPauseDisclosureDropdownItem);
+ const findDeleteItem = () => findDropdown().findComponent(RunnerDeleteDisclosureDropdownItem);
+
+ const createComponent = ({ props = {}, options = {}, mountFn = shallowMountExtended } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = mountFn(RunnerHeaderActions, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ editPath: mockRunnerEditPath,
+ ...propsData,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders all elements', () => {
+ // visible on md and up screens
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ expect(findRunnerDeleteButton().exists()).toBe(true);
+
+ // visible on small screens
+ expect(findDropdown().exists()).toBe(true);
+ expect(findEditItem().exists()).toBe(true);
+ expect(findPauseItem().exists()).toBe(true);
+ expect(findDeleteItem().exists()).toBe(true);
+ });
+
+ it('renders disclosure dropdown with no caret and accesible text', () => {
+ expect(findDropdown().props()).toMatchObject({
+ icon: 'ellipsis_v',
+ toggleText: s__('Runner|Runner actions'),
+ textSrOnly: true,
+ category: 'tertiary',
+ noCaret: true,
+ });
+ });
+
+ it.each([findRunnerEditButton, findEditItem])('edit path is set (%p)', (find) => {
+ expect(find().props('href')).toEqual(mockRunnerEditPath);
+ });
+
+ it.each([findRunnerDeleteButton, findDeleteItem])('delete is emitted (%p)', (find) => {
+ const deleteEvent = { message: 'Deleted!' };
+
+ find().vm.$emit('deleted', deleteEvent);
+
+ expect(wrapper.emitted('deleted')).toEqual([[deleteEvent]]);
+ });
+
+ describe('when delete is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ userPermissions: {
+ updateRunner: true,
+ deleteRunner: false,
+ },
+ },
+ },
+ });
+ });
+
+ it('does not render delete actions', () => {
+ expect(findRunnerDeleteButton().exists()).toBe(false);
+ expect(findDeleteItem().exists()).toBe(false);
+ });
+ });
+
+ describe('when update is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ userPermissions: {
+ updateRunner: false,
+ deleteRunner: true,
+ },
+ },
+ },
+ });
+ });
+
+ it('does not render delete actions', () => {
+ expect(findRunnerEditButton().exists()).toBe(false);
+ expect(findRunnerPauseButton().exists()).toBe(false);
+ expect(findEditItem().exists()).toBe(false);
+ expect(findPauseItem().exists()).toBe(false);
+ });
+ });
+
+ describe('when no actions are enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ userPermissions: {
+ updateRunner: false,
+ deleteRunner: false,
+ },
+ },
+ },
+ });
+ });
+
+ it('does not render actions', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+});
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 22797433b58..511ed88f5ab 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
@@ -10,7 +10,6 @@ import {
I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER,
- I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH,
} from '~/ci/runner/constants';
@@ -59,136 +58,84 @@ describe('RunnerListEmptyState', () => {
});
describe('when search is not filtered', () => {
- describe.each([
- { createRunnerWorkflowForAdmin: true },
- { createRunnerWorkflowForNamespace: true },
- ])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
- beforeEach(() => {
- glFeatures = currentGlFeatures;
- });
-
- describe.each`
- newRunnerPath | registrationToken | expectedMessages
- ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
- ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
- ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
- ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
- `(
- 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
- ({ newRunnerPath, registrationToken, expectedMessages }) => {
- beforeEach(() => {
- createComponent({
- props: {
- newRunnerPath,
- registrationToken,
- },
- });
- });
-
- it('shows title', () => {
- expectTitleToBe(I18N_GET_STARTED);
- });
-
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
-
- it(`shows description: "${expectedMessages.join(' ')}"`, () => {
- expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
- });
- },
- );
-
- describe('with newRunnerPath and registration token', () => {
+ describe.each`
+ newRunnerPath | registrationToken | expectedMessages
+ ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
+ ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
+ `(
+ 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
+ ({ newRunnerPath, registrationToken, expectedMessages }) => {
beforeEach(() => {
createComponent({
props: {
- registrationToken: mockRegistrationToken,
- newRunnerPath: mockNewRunnerPath,
+ newRunnerPath,
+ registrationToken,
},
});
});
- it('shows links to the new runner page and registration instructions', () => {
- expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
+ it('shows title', () => {
+ expectTitleToBe(I18N_GET_STARTED);
+ });
- const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
- });
- describe('with newRunnerPath and no registration token', () => {
- beforeEach(() => {
- createComponent({
- props: {
- registrationToken: mockRegistrationToken,
- newRunnerPath: null,
- },
- });
+ it(`shows description: "${expectedMessages.join(' ')}"`, () => {
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
});
+ },
+ );
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ describe('with newRunnerPath and registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: mockNewRunnerPath,
+ },
});
});
- describe('with no newRunnerPath nor registration token', () => {
- beforeEach(() => {
- createComponent({
- props: {
- registrationToken: null,
- newRunnerPath: null,
- },
- });
- });
+ it('shows links to the new runner page and registration instructions', () => {
+ expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
- it('has no link', () => {
- expect(findLink().exists()).toBe(false);
- });
+ const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
- describe('when createRunnerWorkflow is disabled', () => {
- describe('when there is a registration token', () => {
- beforeEach(() => {
- createComponent({
- props: {
- registrationToken: mockRegistrationToken,
- },
- });
- });
-
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
-
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
- });
-
- it('displays text with registration instructions', () => {
- expectTitleToBe(I18N_GET_STARTED);
-
- expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
+ describe('with newRunnerPath and no registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: null,
+ },
});
});
- describe('when there is no registration token', () => {
- beforeEach(() => {
- createComponent({ props: { registrationToken: null } });
- });
-
- it('displays "contact admin" text', () => {
- expectTitleToBe(I18N_GET_STARTED);
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+ });
- expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
+ describe('with no newRunnerPath nor registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: null,
+ newRunnerPath: null,
+ },
});
+ });
- it('has no registration instructions link', () => {
- expect(findLink().exists()).toBe(false);
- });
+ it('has no link', () => {
+ expect(findLink().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ci/runner/components/runner_pause_action_spec.js b/spec/frontend/ci/runner/components/runner_pause_action_spec.js
new file mode 100644
index 00000000000..b987eb1e310
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_pause_action_spec.js
@@ -0,0 +1,180 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { createAlert } from '~/alert';
+
+import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue';
+import { allRunnersData } from '../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+describe('RunnerPauseAction', () => {
+ let wrapper;
+ let runnerTogglePausedHandler;
+
+ const findBtn = () => wrapper.find('button');
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = mountFn(RunnerPauseAction, {
+ propsData: {
+ runner: {
+ id: mockRunner.id,
+ paused: mockRunner.paused,
+ ...runner,
+ },
+ ...propsData,
+ },
+ apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]),
+ scopedSlots: {
+ default: '<button :disabled="props.loading" @click="props.onClick"/>',
+ },
+ });
+ };
+
+ const clickAndWait = async () => {
+ findBtn().trigger('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => {
+ return Promise.resolve({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: input.id,
+ paused: !input.paused,
+ },
+ errors: [],
+ },
+ },
+ });
+ });
+
+ createComponent();
+ });
+
+ describe('Pause/Resume action', () => {
+ describe.each`
+ runnerState | isPaused | newPausedValue
+ ${'paused'} | ${true} | ${false}
+ ${'active'} | ${false} | ${true}
+ `('When the runner is $runnerState', ({ isPaused, newPausedValue }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ paused: isPaused,
+ },
+ },
+ });
+ });
+
+ it('Displays slot contents', () => {
+ expect(findBtn().exists()).toBe(true);
+ });
+
+ it('The mutation has not been called', () => {
+ expect(runnerTogglePausedHandler).not.toHaveBeenCalled();
+ });
+
+ describe('Immediately after the action is triggered', () => {
+ it('The button has a loading state', async () => {
+ await findBtn().trigger('click');
+
+ expect(findBtn().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('After the action is triggered', () => {
+ beforeEach(async () => {
+ await clickAndWait();
+ });
+
+ it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => {
+ expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1);
+ expect(runnerTogglePausedHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ paused: newPausedValue,
+ },
+ });
+ });
+
+ it('The button does not have a loading state', () => {
+ expect(findBtn().attributes('disabled')).toBeUndefined();
+ });
+
+ it('The button emits "done"', () => {
+ expect(wrapper.emitted('done')).toHaveLength(1);
+ });
+ });
+
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(mockErrorMsg),
+ component: 'RunnerPauseAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerTogglePausedHandler.mockResolvedValueOnce({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: mockRunner.id,
+ paused: isPaused,
+ },
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerPauseAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+ });
+ });
+});
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 1ea870e004a..f1ceecd4ae4 100644
--- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -1,13 +1,7 @@
-import Vue, { nextTick } from 'vue';
import { GlButton } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql';
-import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/ci/runner/sentry_utils';
-import { createAlert } from '~/alert';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
@@ -16,244 +10,140 @@ import {
} from '~/ci/runner/constants';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
-import { allRunnersData } from '../mock_data';
-
-const mockRunner = allRunnersData.data.runners.nodes[0];
-
-Vue.use(VueApollo);
-
-jest.mock('~/alert');
-jest.mock('~/ci/runner/sentry_utils');
+import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue';
describe('RunnerPauseButton', () => {
let wrapper;
- let runnerTogglePausedHandler;
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+ const findRunnerPauseAction = () => wrapper.findComponent(RunnerPauseAction);
const findBtn = () => wrapper.findComponent(GlButton);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
- const { runner, ...propsData } = props;
-
+ const createComponent = ({
+ props = {},
+ loading,
+ onClick = jest.fn(),
+ mountFn = shallowMountExtended,
+ } = {}) => {
wrapper = mountFn(RunnerPauseButton, {
propsData: {
- runner: {
- id: mockRunner.id,
- paused: mockRunner.paused,
- ...runner,
- },
- ...propsData,
+ runner: {},
+ ...props,
},
- apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]),
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
+ stubs: {
+ RunnerPauseAction: stubComponent(RunnerPauseAction, {
+ render() {
+ return this.$scopedSlots.default({
+ loading,
+ onClick,
+ });
+ },
+ }),
+ },
});
};
- const clickAndWait = async () => {
- findBtn().vm.$emit('click');
- await waitForPromises();
- };
-
beforeEach(() => {
- runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => {
- return Promise.resolve({
- data: {
- runnerUpdate: {
- runner: {
- id: input.id,
- paused: !input.paused,
- },
- errors: [],
- },
- },
- });
- });
-
createComponent();
});
- describe('Pause/Resume action', () => {
+ describe('Pause/Resume button', () => {
describe.each`
- runnerState | icon | content | tooltip | isPaused | newPausedValue
- ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${true} | ${false}
- ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${false} | ${true}
- `('When the runner is $runnerState', ({ icon, content, tooltip, isPaused, newPausedValue }) => {
- beforeEach(() => {
- createComponent({
- props: {
- runner: {
- paused: isPaused,
+ runnerState | paused | expectedIcon | expectedContent | expectedTooltip
+ ${'paused'} | ${true} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP}
+ ${'active'} | ${false} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP}
+ `(
+ 'When the runner is $runnerState',
+ ({ paused, expectedIcon, expectedContent, expectedTooltip }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: { paused },
},
- },
- });
- });
-
- it(`Displays a ${icon} button`, () => {
- expect(findBtn().props('loading')).toBe(false);
- expect(findBtn().props('icon')).toBe(icon);
- });
-
- it('Displays button content', () => {
- expect(findBtn().text()).toBe(content);
- expect(getTooltip()).toBe(tooltip);
- });
-
- it('Does not display redundant text for screen readers', () => {
- expect(findBtn().attributes('aria-label')).toBe(undefined);
- });
-
- describe(`Before the ${icon} button is clicked`, () => {
- it('The mutation has not been called', () => {
- expect(runnerTogglePausedHandler).not.toHaveBeenCalled();
+ });
});
- });
-
- describe(`Immediately after the ${icon} button is clicked`, () => {
- const setup = async () => {
- findBtn().vm.$emit('click');
- await nextTick();
- };
- it('The button has a loading state', async () => {
- await setup();
-
- expect(findBtn().props('loading')).toBe(true);
+ it(`Displays a ${expectedIcon} button`, () => {
+ expect(findBtn().props('loading')).toBe(false);
+ expect(findBtn().props('icon')).toBe(expectedIcon);
});
- it('The stale tooltip is removed', async () => {
- await setup();
-
- expect(getTooltip()).toBe('');
+ it('Displays button content', () => {
+ expect(findBtn().text()).toBe(expectedContent);
+ expect(getTooltip()).toBe(expectedTooltip);
});
- });
- describe(`After clicking on the ${icon} button`, () => {
- beforeEach(async () => {
- await clickAndWait();
+ it('Does not display redundant text for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe(undefined);
});
+ },
+ );
+ });
- it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => {
- expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1);
- expect(runnerTogglePausedHandler).toHaveBeenCalledWith({
- input: {
- id: mockRunner.id,
- paused: newPausedValue,
+ describe('Compact button', () => {
+ describe.each`
+ runnerState | paused | expectedIcon | expectedContent | expectedTooltip
+ ${'paused'} | ${true} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP}
+ ${'active'} | ${false} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP}
+ `(
+ 'When the runner is $runnerState',
+ ({ paused, expectedIcon, expectedContent, expectedTooltip }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: { paused },
+ compact: true,
},
+ mountFn: mountExtended,
});
});
- it('The button does not have a loading state', () => {
+ it(`Displays a ${expectedIcon} button`, () => {
expect(findBtn().props('loading')).toBe(false);
+ expect(findBtn().props('icon')).toBe(expectedIcon);
});
- it('The button emits toggledPaused', () => {
- expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
- });
- });
-
- describe('When update fails', () => {
- describe('On a network error', () => {
- const mockErrorMsg = 'Update error!';
-
- beforeEach(async () => {
- runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
-
- await clickAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(mockErrorMsg),
- component: 'RunnerPauseButton',
- });
- });
+ it('Displays button content', () => {
+ expect(findBtn().text()).toBe('');
+ // Note: Use <template v-if> to ensure rendering a
+ // text-less button. Ensure we don't send even empty an
+ // content slot to prevent a distorted/rectangular button.
+ expect(wrapper.find('.gl-button-text').exists()).toBe(false);
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
+ expect(getTooltip()).toBe(expectedTooltip);
});
- describe('On a validation error', () => {
- const mockErrorMsg = 'Runner not found!';
- const mockErrorMsg2 = 'User not allowed!';
-
- beforeEach(async () => {
- runnerTogglePausedHandler.mockResolvedValueOnce({
- data: {
- runnerUpdate: {
- runner: {
- id: mockRunner.id,
- paused: isPaused,
- },
- errors: [mockErrorMsg, mockErrorMsg2],
- },
- },
- });
-
- await clickAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
- component: 'RunnerPauseButton',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
+ it('Does not display redundant text for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe(expectedContent);
});
- });
- });
+ },
+ );
});
- describe('When displaying a compact button for an active runner', () => {
- beforeEach(() => {
- createComponent({
- props: {
- runner: {
- paused: false,
- },
- compact: true,
- },
- mountFn: mountExtended,
- });
- });
-
- it('Displays no text', () => {
- expect(findBtn().text()).toBe('');
+ it('Shows loading state', () => {
+ createComponent({ loading: true });
- // Note: Use <template v-if> to ensure rendering a
- // text-less button. Ensure we don't send even empty an
- // content slot to prevent a distorted/rectangular button.
- expect(wrapper.find('.gl-button-text').exists()).toBe(false);
- });
+ expect(findBtn().props('loading')).toBe(true);
+ expect(getTooltip()).toBe('');
+ });
- it('Display correctly for screen readers', () => {
- expect(findBtn().attributes('aria-label')).toBe(I18N_PAUSE);
- expect(getTooltip()).toBe(I18N_PAUSE_TOOLTIP);
- });
+ it('Triggers action', () => {
+ const mockOnClick = jest.fn();
- describe('Immediately after the button is clicked', () => {
- const setup = async () => {
- findBtn().vm.$emit('click');
- await nextTick();
- };
+ createComponent({ onClick: mockOnClick });
+ findBtn().vm.$emit('click');
- it('The button has a loading state', async () => {
- await setup();
+ expect(mockOnClick).toHaveBeenCalled();
+ });
- expect(findBtn().props('loading')).toBe(true);
- });
+ it('Emits toggledPaused when done', () => {
+ createComponent();
- it('The stale tooltip is removed', async () => {
- await setup();
+ findRunnerPauseAction().vm.$emit('done');
- expect(getTooltip()).toBe('');
- });
- });
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js b/spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js
new file mode 100644
index 00000000000..5dc9a615b0e
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js
@@ -0,0 +1,71 @@
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { I18N_PAUSE, I18N_RESUME } from '~/ci/runner/constants';
+
+import RunnerPauseDisclosureDropdownItem from '~/ci/runner/components/runner_pause_disclosure_dropdown_item.vue';
+import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue';
+
+describe('RunnerPauseButton', () => {
+ let wrapper;
+
+ const findRunnerPauseAction = () => wrapper.findComponent(RunnerPauseAction);
+ const findDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ const createComponent = ({
+ props = {},
+ onClick = jest.fn(),
+ mountFn = shallowMountExtended,
+ } = {}) => {
+ wrapper = mountFn(RunnerPauseDisclosureDropdownItem, {
+ propsData: {
+ runner: {},
+ ...props,
+ },
+ stubs: {
+ RunnerPauseAction: stubComponent(RunnerPauseAction, {
+ render() {
+ return this.$scopedSlots.default({
+ onClick,
+ });
+ },
+ }),
+ },
+ });
+ };
+
+ it('Displays paused runner button content', () => {
+ createComponent({
+ props: { runner: { paused: true } },
+ mountFn: mountExtended,
+ });
+
+ expect(findDisclosureDropdownItem().text()).toBe(I18N_RESUME);
+ });
+
+ it('Displays active runner button content', () => {
+ createComponent({
+ props: { runner: { paused: false } },
+ mountFn: mountExtended,
+ });
+
+ expect(findDisclosureDropdownItem().text()).toBe(I18N_PAUSE);
+ });
+
+ it('Triggers action', () => {
+ const mockOnClick = jest.fn();
+
+ createComponent({ onClick: mockOnClick });
+ findDisclosureDropdownItem().vm.$emit('action');
+
+ expect(mockOnClick).toHaveBeenCalled();
+ });
+
+ it('Emits toggledPaused when done', () => {
+ createComponent();
+
+ findRunnerPauseAction().vm.$emit('done');
+
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
+});
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 120388900b5..7438c47e32c 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
@@ -9,10 +9,8 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
import RunnerDetails from '~/ci/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
@@ -47,9 +45,7 @@ describe('GroupRunnerShowApp', () => {
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
- const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
- const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
- const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerHeaderActions = () => wrapper.findComponent(RunnerHeaderActions);
const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
@@ -95,10 +91,11 @@ describe('GroupRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`#${mockRunnerId} (${mockRunnerSha})`);
});
- it('displays the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
- expect(findRunnerPauseButton().exists()).toBe(true);
- expect(findRunnerDeleteButton().exists()).toBe(true);
+ it('displays the runner buttons', () => {
+ expect(findRunnerHeaderActions().props()).toEqual({
+ runner: mockRunner,
+ editPath: mockEditGroupRunnerPath,
+ });
});
it('shows runner details', () => {
@@ -127,54 +124,6 @@ describe('GroupRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- describe('when runner cannot be updated', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- updateRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(false);
- expect(findRunnerPauseButton().exists()).toBe(false);
- });
-
- it('displays delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(true);
- });
- });
-
- describe('when runner cannot be deleted', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- deleteRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(false);
- });
-
- it('displays edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(true);
- expect(findRunnerPauseButton().exists()).toBe(true);
- });
- });
-
describe('when runner is deleted', () => {
beforeEach(async () => {
await createComponent({
@@ -183,7 +132,7 @@ describe('GroupRunnerShowApp', () => {
});
it('redirects to the runner list page', () => {
- findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerHeaderActions().vm.$emit('deleted', { message: 'Runner deleted' });
expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
message: 'Runner deleted',
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 74eeb864cd8..f3d7ae85e0d 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
@@ -483,35 +483,15 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(true);
});
- it('when create_runner_workflow_for_namespace is enabled', () => {
+ it('shows the create runner button', () => {
createComponent({
props: {
newRunnerPath,
},
- provide: {
- glFeatures: {
- createRunnerWorkflowForNamespace: true,
- },
- },
});
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
});
-
- it('when create_runner_workflow_for_namespace is disabled', () => {
- createComponent({
- props: {
- newRunnerPath,
- },
- provide: {
- glFeatures: {
- createRunnerWorkflowForNamespace: false,
- },
- },
- });
-
- expect(findNewRunnerBtn().exists()).toBe(false);
- });
});
describe('when user has no permission to register group runner', () => {
@@ -524,16 +504,11 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(false);
});
- it('when create_runner_workflow_for_namespace is enabled', () => {
+ it('shows the create runner button', () => {
createComponent({
props: {
newRunnerPath: null,
},
- provide: {
- glFeatures: {
- createRunnerWorkflowForNamespace: true,
- },
- },
});
expect(findNewRunnerBtn().exists()).toBe(false);
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 f0fded7b7b2..40cb3b8292f 100644
--- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
@@ -3,6 +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 { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import {
@@ -37,6 +38,7 @@ describe('CreateTokenModal', () => {
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
+ const hideModalMock = jest.fn();
const provide = {
agentName,
@@ -91,10 +93,12 @@ describe('CreateTokenModal', () => {
provide,
propsData,
stubs: {
- GlModal,
+ GlModal: stubComponent(GlModal, {
+ methods: { hide: hideModalMock },
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
},
});
- wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
@@ -138,6 +142,11 @@ describe('CreateTokenModal', () => {
expectDisabledAttribute(findCancelButton(), false);
});
+ it('cancel button should hide the modal', () => {
+ findCancelButton().vm.$emit('click');
+ expect(hideModalMock).toHaveBeenCalled();
+ });
+
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
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 970782a8e58..de47ff78696 100644
--- a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
@@ -2,6 +2,7 @@ import { GlButton, GlModal, GlFormInput, GlTooltip } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -81,12 +82,15 @@ describe('RevokeTokenButton', () => {
},
propsData,
stubs: {
- GlModal,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: jest.fn(),
+ },
+ }),
GlTooltip,
},
mocks: { $toast: { show: toast } },
});
- wrapper.vm.$refs.modal.hide = jest.fn();
writeQuery();
await nextTick();
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index e4e1986f705..6957862dc2b 100644
--- a/spec/frontend/clusters_list/components/clusters_actions_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -1,4 +1,10 @@
-import { GlButton, GlDropdown, GlDropdownItem, GlTooltip } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlTooltip,
+ GlButtonGroup,
+} from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -18,12 +24,13 @@ describe('ClustersActionsComponent', () => {
certificateBasedClustersEnabled: true,
};
+ const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const findButton = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdownItemIds = () =>
- findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
+ findDropdownItems().wrappers.map((x) => x.find('a').attributes('data-testid'));
const findDropdownItemTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findNewClusterDocsLink = () => wrapper.findByTestId('create-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
@@ -34,6 +41,10 @@ describe('ClustersActionsComponent', () => {
...defaultProvide,
...provideData,
},
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
directives: {
GlModalDirective: createMockDirective('gl-modal-directive'),
},
@@ -45,25 +56,23 @@ describe('ClustersActionsComponent', () => {
});
describe('when the certificate based clusters are enabled', () => {
- it('renders actions menu', () => {
+ it('renders actions menu button group with dropdown', () => {
+ expect(findButtonGroup().exists()).toBe(true);
+ expect(findButton().exists()).toBe(true);
expect(findDropdown().exists()).toBe(true);
});
- it('shows split button in dropdown', () => {
- expect(findDropdown().props('split')).toBe(true);
- });
-
it("doesn't show the tooltip", () => {
expect(findTooltip().exists()).toBe(false);
});
describe('when on project level', () => {
it(`displays default action as ${CLUSTERS_ACTIONS.connectWithAgent}`, () => {
- expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectWithAgent);
+ expect(findButton().text()).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
it('renders correct modal id for the default action', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ const binding = getBinding(findButton().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
@@ -91,6 +100,7 @@ describe('ClustersActionsComponent', () => {
it('disables dropdown', () => {
expect(findDropdown().props('disabled')).toBe(true);
+ expect(findButton().props('disabled')).toBe(true);
});
it('shows tooltip explaining why dropdown is disabled', () => {
@@ -98,7 +108,7 @@ describe('ClustersActionsComponent', () => {
});
it('does not bind split dropdown button', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ const binding = getBinding(findButton().element, 'gl-modal-directive');
expect(binding.value).toBe(false);
});
@@ -148,11 +158,11 @@ describe('ClustersActionsComponent', () => {
});
it(`displays default action as ${CLUSTERS_ACTIONS.connectCluster}`, () => {
- expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectCluster);
+ expect(findButton().text()).toBe(CLUSTERS_ACTIONS.connectCluster);
});
it('renders correct modal id for the default action', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ const binding = getBinding(findButton().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
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 8bbb5ec92a7..afb12d9c856 100644
--- a/spec/frontend/clusters_list/components/delete_agent_button_spec.js
+++ b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
@@ -9,6 +9,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
import { DELETE_AGENT_BUTTON } from '~/clusters_list/constants';
+import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
@@ -84,9 +85,14 @@ describe('DeleteAgentButton', () => {
},
propsData,
mocks: { $toast: { show: toast } },
- stubs: { GlModal },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: jest.fn(),
+ },
+ }),
+ },
});
- wrapper.vm.$refs.modal.hide = jest.fn();
writeQuery();
await nextTick();
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
index 8cad483e27e..d0bc7a55f8e 100644
--- a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -130,7 +130,7 @@ exports[`Comment templates list item component renders list item 1`] = `
</div>
<div
- class="gl-mt-3 gl-font-monospace"
+ class="gl-mt-3 gl-font-monospace gl-white-space-pre-wrap"
>
/assign_reviewer
</div>
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_spec.js
index e474ef9c635..73031724b12 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { createAlert } from '~/alert';
import Poll from '~/lib/utils/poll';
-import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/poll');
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 85eafa9e85c..53c098ee153 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
@@ -65,7 +65,7 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
onHidden: expect.any(Function),
onShow: expect.any(Function),
strategy: 'fixed',
- maxWidth: 'auto',
+ maxWidth: '400px',
...tippyOptions,
}),
});
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 c79df9c9ed8..b219c506753 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
@@ -206,7 +206,7 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('remove-link').vm.$emit('click');
- expect(tiptapEditor.getHTML()).toBe('<p>Download PDF File</p>');
+ expect(tiptapEditor.getHTML()).toBe('<p dir="auto">Download PDF File</p>');
});
});
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 89beb76a6f2..002e19ee8cf 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
@@ -22,19 +22,19 @@ import {
PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../../test_constants';
-const TIPTAP_AUDIO_HTML = `<p>
+const TIPTAP_AUDIO_HTML = `<p dir="auto">
<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" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
-const TIPTAP_DIAGRAM_HTML = `<p>
- <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
+const TIPTAP_DIAGRAM_HTML = `<p dir="auto">
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon">
</p>`;
-const TIPTAP_IMAGE_HTML = `<p>
- <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
+const TIPTAP_IMAGE_HTML = `<p dir="auto">
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon">
</p>`;
-const TIPTAP_VIDEO_HTML = `<p>
+const TIPTAP_VIDEO_HTML = `<p dir="auto">
<span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
@@ -101,9 +101,7 @@ describe.each`
const expectLinkButtonsToExist = (exist = true) => {
expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
- expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist);
expect(wrapper.findByTestId('edit-media').exists()).toBe(exist);
- expect(wrapper.findByTestId('delete-media').exists()).toBe(exist);
};
beforeEach(() => {
@@ -128,14 +126,11 @@ describe.each`
await buildWrapperAndDisplayMenu();
const link = wrapper.findComponent(GlLink);
- expect(link.attributes()).toEqual(
- expect.objectContaining({
- href: `/group1/project1/-/wikis/${filePath}`,
- 'aria-label': filePath,
- title: filePath,
- target: '_blank',
- }),
- );
+ expect(link.attributes()).toMatchObject({
+ href: `/group1/project1/-/wikis/${filePath}`,
+ 'aria-label': filePath,
+ target: '_blank',
+ });
expect(link.text()).toBe(filePath);
});
@@ -190,28 +185,6 @@ describe.each`
});
});
- describe('copy button', () => {
- it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
- await buildWrapperAndDisplayMenu();
-
- jest.spyOn(navigator.clipboard, 'writeText');
-
- await wrapper.findByTestId('copy-media-src').vm.$emit('click');
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath);
- });
- });
-
- describe(`remove ${mediaType} button`, () => {
- it(`removes the ${mediaType}`, async () => {
- await buildWrapperAndDisplayMenu();
-
- await wrapper.findByTestId('delete-media').vm.$emit('click');
-
- expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
- });
- });
-
describe(`replace ${mediaType} button`, () => {
beforeEach(buildWrapperAndDisplayMenu);
@@ -252,7 +225,6 @@ describe.each`
describe('edit button', () => {
let mediaSrcInput;
- let mediaTitleInput;
let mediaAltInput;
beforeEach(async () => {
@@ -261,7 +233,6 @@ describe.each`
await wrapper.findByTestId('edit-media').vm.$emit('click');
mediaSrcInput = wrapper.findByTestId('media-src');
- mediaTitleInput = wrapper.findByTestId('media-title');
mediaAltInput = wrapper.findByTestId('media-alt');
});
@@ -269,11 +240,10 @@ describe.each`
expectLinkButtonsToExist(false);
});
- it(`shows a form to edit the ${mediaType} src/title/alt`, () => {
+ it(`shows a form to edit the ${mediaType} src/alt`, () => {
expect(wrapper.findComponent(GlForm).exists()).toBe(true);
expect(mediaSrcInput.element.value).toBe(filePath);
- expect(mediaTitleInput.element.value).toBe('');
expect(mediaAltInput.element.value).toBe('test-file');
});
@@ -281,7 +251,6 @@ describe.each`
beforeEach(async () => {
mediaSrcInput.setValue('https://gitlab.com/favicon.png');
mediaAltInput.setValue('gitlab favicon');
- mediaTitleInput.setValue('gitlab favicon');
contentEditor.resolveUrl.mockResolvedValue('https://gitlab.com/favicon.png');
@@ -294,14 +263,11 @@ describe.each`
it(`updates the link to the ${mediaType} in the bubble menu`, () => {
const link = wrapper.findComponent(GlLink);
- expect(link.attributes()).toEqual(
- expect.objectContaining({
- href: 'https://gitlab.com/favicon.png',
- 'aria-label': 'https://gitlab.com/favicon.png',
- title: 'https://gitlab.com/favicon.png',
- target: '_blank',
- }),
- );
+ expect(link.attributes()).toMatchObject({
+ href: 'https://gitlab.com/favicon.png',
+ 'aria-label': 'https://gitlab.com/favicon.png',
+ target: '_blank',
+ });
expect(link.text()).toBe('https://gitlab.com/favicon.png');
});
});
@@ -310,7 +276,6 @@ describe.each`
beforeEach(async () => {
mediaSrcInput.setValue('https://gitlab.com/favicon.png');
mediaAltInput.setValue('gitlab favicon');
- mediaTitleInput.setValue('gitlab favicon');
await wrapper.findByTestId('cancel-editing-media').vm.$emit('click');
});
@@ -324,12 +289,10 @@ describe.each`
await wrapper.findByTestId('edit-media').vm.$emit('click');
mediaSrcInput = wrapper.findByTestId('media-src');
- mediaTitleInput = wrapper.findByTestId('media-title');
mediaAltInput = wrapper.findByTestId('media-alt');
expect(mediaSrcInput.element.value).toBe(filePath);
expect(mediaAltInput.element.value).toBe('test-file');
- expect(mediaTitleInput.element.value).toBe('');
});
});
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
index 169f77dc054..c46aa1b657e 100644
--- a/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
@@ -241,7 +241,7 @@ describe('content_editor/components/bubble_menus/reference_bubble_menu', () => {
await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('remove-reference').trigger('click');
- expect(tiptapEditor.getHTML()).toBe('<p></p>');
+ expect(tiptapEditor.getHTML()).toBe('<p dir="auto"></p>');
});
});
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 0b8321ba8eb..816c9458201 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -14,6 +14,7 @@ import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vu
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
jest.mock('~/emoji');
@@ -92,19 +93,6 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
});
- it('renders footer containing quick actions help text if quick actions docs path is defined', () => {
- createWrapper({ quickActionsDocsPath: '/foo/bar' });
-
- expect(wrapper.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('displays an attachment button', () => {
createWrapper();
@@ -286,4 +274,10 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(component).exists()).toBe(true);
});
+
+ it('renders an editor mode dropdown', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 9d835381ff4..6562cb517cd 100644
--- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -2,24 +2,31 @@ 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';
+import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/formatting_toolbar', () => {
let wrapper;
let trackingSpy;
- const buildWrapper = (props) => {
+ const contentEditor = {
+ codeSuggestionsConfig: {
+ canSuggest: true,
+ },
+ };
+
+ const buildWrapper = ({ props = {}, provide = { contentEditor } } = {}) => {
wrapper = shallowMountExtended(FormattingToolbar, {
stubs: {
GlTabs,
GlTab,
- EditorModeSwitcher,
},
propsData: props,
+ provide,
});
};
@@ -28,20 +35,22 @@ describe('content_editor/components/formatting_toolbar', () => {
});
describe.each`
- testId | controlProps
- ${'text-styles'} | ${{}}
- ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
- ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
- ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
- ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
- ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
- ${'link'} | ${{}}
- ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
- ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
- ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
- ${'attachment'} | ${{}}
- ${'table'} | ${{}}
- ${'more'} | ${{}}
+ testId | controlProps
+ ${'text-styles'} | ${{}}
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold (Ctrl+B)', editorCommand: 'toggleBold' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic (Ctrl+I)', editorCommand: 'toggleItalic' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough (Ctrl+Shift+X)', editorCommand: 'toggleStrike' }}
+ ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
+ ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link (Ctrl+K)', editorCommand: 'editLink' }}
+ ${'link'} | ${{}}
+ ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
+ ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
+ ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
+ ${'code-suggestion'} | ${{ contentType: 'codeSuggestion', iconName: 'doc-code', label: 'Insert suggestion', editorCommand: 'insertCodeSuggestion' }}
+ ${'attachment'} | ${{}}
+ ${'table'} | ${{}}
+ ${'more'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
@@ -69,17 +78,70 @@ describe('content_editor/components/formatting_toolbar', () => {
});
});
- it('renders an editor mode dropdown', () => {
- buildWrapper();
+ describe('MacOS shortcuts', () => {
+ beforeEach(() => {
+ window.gl = { client: { isMac: true } };
+
+ buildWrapper();
+ });
- expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
+ it.each`
+ testId | label
+ ${'bold'} | ${'Bold (⌘B)'}
+ ${'italic'} | ${'Italic (⌘I)'}
+ ${'strike'} | ${'Strikethrough (⌘⇧X)'}
+ ${'link'} | ${'Insert link (⌘K)'}
+ `('shows label $label for $testId', ({ testId, label }) => {
+ expect(wrapper.findByTestId(testId).props('label')).toBe(label);
+ });
});
describe('when attachment button is hidden', () => {
it('does not show the attachment button', () => {
- buildWrapper({ hideAttachmentButton: true });
+ buildWrapper({ props: { hideAttachmentButton: true } });
expect(wrapper.findByTestId('attachment').exists()).toBe(false);
});
});
+
+ describe('when selecting a saved reply from the comment templates dropdown', () => {
+ it('updates the rich text editor with the saved comment', async () => {
+ const tiptapEditor = createTestEditor();
+
+ buildWrapper({
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ newCommentTemplatePath: 'some/path',
+ },
+ });
+
+ const commands = mockChainedCommands(tiptapEditor, ['focus', 'pasteContent', 'run']);
+ await wrapper
+ .findComponent(CommentTemplatesDropdown)
+ .vm.$emit('select', 'Some saved comment');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.pasteContent).toHaveBeenCalledWith('Some saved comment');
+ expect(commands.run).toHaveBeenCalled();
+ });
+
+ it('does not show the saved replies icon if newCommentTemplatePath is not provided', () => {
+ buildWrapper();
+
+ expect(wrapper.findComponent(CommentTemplatesDropdown).exists()).toBe(false);
+ });
+ });
+
+ it('hides code suggestions icon if the user cannot make suggestions', () => {
+ buildWrapper({
+ provide: {
+ contentEditor: {
+ codeSuggestionsConfig: { canSuggest: false },
+ },
+ },
+ });
+
+ expect(wrapper.findByTestId('code-suggestion').exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
index 9d34d9d0e9e..ee3ad59bf9a 100644
--- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
@@ -113,7 +113,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps}
`(
'runs a command to insert the selected $referenceType',
- ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => {
+ async ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => {
const commandSpy = jest.fn();
buildWrapper({
@@ -129,7 +129,10 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
},
});
- wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ await wrapper
+ .findByTestId('content-editor-suggestions-dropdown')
+ .find('li .gl-new-dropdown-item-content')
+ .trigger('click');
expect(commandSpy).toHaveBeenCalledWith(
expect.objectContaining({
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 cbeea90dcb4..e802681dfc6 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -6,11 +6,26 @@ import eventHubFactory from '~/helpers/event_hub_factory';
import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import Diagram from '~/content_editor/extensions/diagram';
+import CodeSuggestion from '~/content_editor/extensions/code_suggestion';
import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
-import { emitEditorEvent, createTestEditor } from '../../test_utils';
+import { emitEditorEvent, createTestEditor, mockChainedCommands } from '../../test_utils';
+
+const SAMPLE_README_CONTENT = `# Sample README
+
+This is a sample README.
+
+## Usage
+
+\`\`\`yaml
+foo: bar
+\`\`\`
+`;
jest.mock('~/content_editor/services/code_block_language_loader');
+jest.mock('~/content_editor/services/utils', () => ({
+ memoizedGet: jest.fn().mockResolvedValue(SAMPLE_README_CONTENT),
+}));
describe('content/components/wrappers/code_block', () => {
const language = 'yaml';
@@ -21,7 +36,7 @@ describe('content/components/wrappers/code_block', () => {
let eventHub;
const buildEditor = () => {
- tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] });
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram, CodeSuggestion] });
contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') };
eventHub = eventHubFactory();
};
@@ -76,7 +91,7 @@ describe('content/components/wrappers/code_block', () => {
it('renders label indicating that code block is frontmatter', () => {
createWrapper({ isFrontmatter: true, language });
- const label = wrapper.find('[data-testid="frontmatter-label"]');
+ const label = wrapper.findByTestId('frontmatter-label');
expect(label.text()).toEqual('frontmatter:yaml');
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
@@ -143,4 +158,222 @@ describe('content/components/wrappers/code_block', () => {
expect(wrapper.find('img').exists()).toBe(false);
});
});
+
+ describe('code suggestions', () => {
+ const nodeAttrs = { language: 'suggestion', isCodeSuggestion: true, langParams: '-0+0' };
+ const findCodeSuggestionBoxText = () =>
+ wrapper.findByTestId('code-suggestion-box').text().replace(/\s+/gm, ' ');
+ const findCodeDeleted = () =>
+ wrapper
+ .findByTestId('suggestion-deleted')
+ .findAll('code')
+ .wrappers.map((w) => w.html())
+ .join('\n');
+ const findCodeAdded = () =>
+ wrapper
+ .findByTestId('suggestion-added')
+ .findAll('code')
+ .wrappers.map((w) => w.html())
+ .join('\n');
+
+ let commands;
+
+ const clickButton = async ({ button, expectedLangParams }) => {
+ await button.trigger('click');
+
+ expect(commands.updateAttributes).toHaveBeenCalledWith('codeSuggestion', {
+ langParams: expectedLangParams,
+ });
+ expect(commands.run).toHaveBeenCalled();
+
+ await wrapper.setProps({ node: { attrs: { ...nodeAttrs, langParams: expectedLangParams } } });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ };
+
+ beforeEach(async () => {
+ contentEditor = {
+ codeSuggestionsConfig: {
+ canSuggest: true,
+ line: { new_line: 5 },
+ lines: [{ new_line: 5 }],
+ showPopover: false,
+ diffFile: {
+ view_path:
+ '/gitlab-org/gitlab-test/-/blob/468abc807a2b2572f43e72c743b76cee6db24025/README.md',
+ },
+ },
+ };
+
+ commands = mockChainedCommands(tiptapEditor, ['updateAttributes', 'run']);
+
+ createWrapper(nodeAttrs);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ });
+
+ it('shows a code suggestion block', () => {
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(
+ `"<code data-line-number=\\"5\\">## Usage\u200b</code>"`,
+ );
+ expect(findCodeAdded()).toMatchInlineSnapshot(
+ `"<code data-line-number=\\"5\\">\u200b</code>"`,
+ );
+ });
+
+ describe('decrement line start button', () => {
+ let button;
+
+ beforeEach(() => {
+ button = wrapper.findByTestId('decrement-line-start');
+ });
+
+ it('decrements the start line number', async () => {
+ await clickButton({ button, expectedLangParams: '-1+0' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 4 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"4\\">\u200b
+ </code>
+ <code data-line-number=\\"5\\">## Usage\u200b</code>"
+ `);
+ });
+
+ it('is disabled if the start line is already 1', async () => {
+ expect(button.attributes('disabled')).toBeUndefined();
+
+ await clickButton({ button, expectedLangParams: '-1+0' });
+ await clickButton({ button, expectedLangParams: '-2+0' });
+ await clickButton({ button, expectedLangParams: '-3+0' });
+ await clickButton({ button, expectedLangParams: '-4+0' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 1 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"1\\"># Sample README\u200b
+ </code>
+ <code data-line-number=\\"2\\">\u200b
+ </code>
+ <code data-line-number=\\"3\\">This is a sample README.\u200b
+ </code>
+ <code data-line-number=\\"4\\">\u200b
+ </code>
+ <code data-line-number=\\"5\\">## Usage\u200b</code>"
+ `);
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('increment line start button', () => {
+ let decrementButton;
+ let button;
+
+ beforeEach(() => {
+ decrementButton = wrapper.findByTestId('decrement-line-start');
+ button = wrapper.findByTestId('increment-line-start');
+ });
+
+ it('is disabled if the start line is already the current line', async () => {
+ expect(button.attributes('disabled')).toBe('disabled');
+
+ // decrement once, increment once
+ await clickButton({ button: decrementButton, expectedLangParams: '-1+0' });
+ expect(button.attributes('disabled')).toBeUndefined();
+ await clickButton({ button, expectedLangParams: '-0+0' });
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+
+ it('increments the start line number', async () => {
+ // decrement twice, increment once
+ await clickButton({ button: decrementButton, expectedLangParams: '-1+0' });
+ await clickButton({ button: decrementButton, expectedLangParams: '-2+0' });
+ await clickButton({ button, expectedLangParams: '-1+0' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 4 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"4\\">\u200b
+ </code>
+ <code data-line-number=\\"5\\">## Usage\u200b</code>"
+ `);
+ });
+ });
+
+ describe('decrement line end button', () => {
+ let incrementButton;
+ let button;
+
+ beforeEach(() => {
+ incrementButton = wrapper.findByTestId('increment-line-end');
+ button = wrapper.findByTestId('decrement-line-end');
+ });
+
+ it('is disabled if the line end is already the current line', async () => {
+ expect(button.attributes('disabled')).toBe('disabled');
+
+ // increment once, decrement once
+ await clickButton({ button: incrementButton, expectedLangParams: '-0+1' });
+ expect(button.attributes('disabled')).toBeUndefined();
+ await clickButton({ button, expectedLangParams: '-0+0' });
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+
+ it('increments the end line number', async () => {
+ // increment twice, decrement once
+ await clickButton({ button: incrementButton, expectedLangParams: '-0+1' });
+ await clickButton({ button: incrementButton, expectedLangParams: '-0+2' });
+ await clickButton({ button, expectedLangParams: '-0+1' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 6');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"5\\">## Usage\u200b
+ </code>
+ <code data-line-number=\\"6\\">\u200b</code>"
+ `);
+ });
+ });
+
+ describe('increment line end button', () => {
+ let button;
+
+ beforeEach(() => {
+ button = wrapper.findByTestId('increment-line-end');
+ });
+
+ it('decrements the start line number', async () => {
+ await clickButton({ button, expectedLangParams: '-0+1' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 6');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"5\\">## Usage\u200b
+ </code>
+ <code data-line-number=\\"6\\">\u200b</code>"
+ `);
+ });
+
+ it('is disabled if the end line is EOF', async () => {
+ expect(button.attributes('disabled')).toBeUndefined();
+
+ await clickButton({ button, expectedLangParams: '-0+1' });
+ await clickButton({ button, expectedLangParams: '-0+2' });
+ await clickButton({ button, expectedLangParams: '-0+3' });
+ await clickButton({ button, expectedLangParams: '-0+4' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 9');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"5\\">## Usage\u200b
+ </code>
+ <code data-line-number=\\"6\\">\u200b
+ </code>
+ <code data-line-number=\\"7\\">\`\`\`yaml\u200b
+ </code>
+ <code data-line-number=\\"8\\">foo: bar\u200b
+ </code>
+ <code data-line-number=\\"9\\">\`\`\`\u200b</code>"
+ `);
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/wrappers/image_spec.js b/spec/frontend/content_editor/components/wrappers/image_spec.js
new file mode 100644
index 00000000000..0ac3b7e9465
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/image_spec.js
@@ -0,0 +1,100 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ImageWrapper from '~/content_editor/components/wrappers/image.vue';
+import { createTestEditor, mockChainedCommands } from '../../test_utils';
+
+describe('content/components/wrappers/image_spec', () => {
+ let wrapper;
+ let tiptapEditor;
+
+ const createWrapper = (node = {}) => {
+ tiptapEditor = createTestEditor();
+ wrapper = shallowMountExtended(ImageWrapper, {
+ propsData: {
+ editor: tiptapEditor,
+ node,
+ getPos: jest.fn().mockReturnValue(12),
+ },
+ });
+ };
+
+ const findHandle = (handle) => wrapper.findByTestId(`image-resize-${handle}`);
+ const findImage = () => wrapper.find('img');
+
+ it('renders an image with the given attributes', () => {
+ createWrapper({
+ type: 'image',
+ attrs: { src: 'image.png', alt: 'My Image', width: 200, height: 200 },
+ });
+
+ expect(findImage().attributes()).toMatchObject({
+ src: 'image.png',
+ alt: 'My Image',
+ height: '200',
+ width: '200',
+ });
+ });
+
+ it('sets width and height to auto if not provided', () => {
+ createWrapper({ type: 'image', attrs: { src: 'image.png', alt: 'My Image' } });
+
+ expect(findImage().attributes()).toMatchObject({
+ src: 'image.png',
+ alt: 'My Image',
+ height: 'auto',
+ width: 'auto',
+ });
+ });
+
+ it('renders corner resize handles', () => {
+ createWrapper({ type: 'image', attrs: { src: 'image.png', alt: 'My Image' } });
+
+ expect(findHandle('nw').exists()).toBe(true);
+ expect(findHandle('ne').exists()).toBe(true);
+ expect(findHandle('sw').exists()).toBe(true);
+ expect(findHandle('se').exists()).toBe(true);
+ });
+
+ describe.each`
+ handle | htmlElementAttributes | tiptapNodeAttributes
+ ${'nw'} | ${{ width: '300', height: '75' }} | ${{ width: 300, height: 75 }}
+ ${'ne'} | ${{ width: '500', height: '125' }} | ${{ width: 500, height: 125 }}
+ ${'sw'} | ${{ width: '300', height: '75' }} | ${{ width: 300, height: 75 }}
+ ${'se'} | ${{ width: '500', height: '125' }} | ${{ width: 500, height: 125 }}
+ `('resizing using $handle', ({ handle, htmlElementAttributes, tiptapNodeAttributes }) => {
+ let handleEl;
+
+ const initialMousePosition = { screenX: 200, screenY: 200 };
+ const finalMousePosition = { screenX: 300, screenY: 300 };
+
+ beforeEach(() => {
+ createWrapper({
+ type: 'image',
+ attrs: { src: 'image.png', alt: 'My Image', width: 400, height: 100 },
+ });
+
+ handleEl = findHandle(handle);
+ handleEl.element.dispatchEvent(new MouseEvent('mousedown', initialMousePosition));
+ document.dispatchEvent(new MouseEvent('mousemove', finalMousePosition));
+ });
+
+ it('resizes the image properly on mousedown+mousemove', () => {
+ expect(findImage().attributes()).toMatchObject(htmlElementAttributes);
+ });
+
+ it('updates prosemirror doc state on mouse release with final size', () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'updateAttributes',
+ 'setNodeSelection',
+ 'run',
+ ]);
+
+ document.dispatchEvent(new MouseEvent('mouseup'));
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.updateAttributes).toHaveBeenCalledWith('image', tiptapNodeAttributes);
+ expect(commands.setNodeSelection).toHaveBeenCalledWith(12);
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/reference_spec.js b/spec/frontend/content_editor/components/wrappers/reference_spec.js
index 828b92a6b1e..132e0e52ae5 100644
--- a/spec/frontend/content_editor/components/wrappers/reference_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/reference_spec.js
@@ -1,4 +1,5 @@
import { GlLink } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ReferenceWrapper from '~/content_editor/components/wrappers/reference.vue';
@@ -8,6 +9,13 @@ describe('content/components/wrappers/reference', () => {
const createWrapper = (node = {}) => {
wrapper = shallowMountExtended(ReferenceWrapper, {
propsData: { node },
+ provide: {
+ contentEditor: {
+ resolveReference: jest.fn().mockResolvedValue({
+ href: 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/252522',
+ }),
+ },
+ },
});
};
@@ -43,4 +51,14 @@ describe('content/components/wrappers/reference', () => {
expect(link.text()).toBe('@root');
expect(link.classes('current-user')).toBe(true);
});
+
+ it('renders the href of the reference correctly', async () => {
+ createWrapper({ attrs: { referenceType: 'issue', text: '#252522' } });
+ await waitForPromises();
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes('href')).toBe(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/252522',
+ );
+ });
});
diff --git a/spec/frontend/content_editor/extensions/code_suggestion_spec.js b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
new file mode 100644
index 00000000000..86656fb96c3
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
@@ -0,0 +1,128 @@
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import CodeSuggestion from '~/content_editor/extensions/code_suggestion';
+import {
+ createTestEditor,
+ createDocBuilder,
+ triggerNodeInputRule,
+ expectDocumentAfterTransaction,
+ sleep,
+} from '../test_utils';
+
+const SAMPLE_README_CONTENT = `# Sample README
+
+This is a sample README.
+
+## Usage
+
+\`\`\`yaml
+foo: bar
+\`\`\`
+`;
+
+jest.mock('~/content_editor/services/utils', () => ({
+ memoizedGet: jest.fn().mockResolvedValue(SAMPLE_README_CONTENT),
+}));
+
+describe('content_editor/extensions/code_suggestion', () => {
+ let tiptapEditor;
+ let doc;
+ let codeSuggestion;
+
+ const codeSuggestionConfig = {
+ canSuggest: true,
+ line: { new_line: 5 },
+ lines: [{ new_line: 5 }],
+ showPopover: false,
+ diffFile: {
+ view_path:
+ '/gitlab-org/gitlab-test/-/blob/468abc807a2b2572f43e72c743b76cee6db24025/README.md',
+ },
+ };
+
+ const createEditor = (config = {}) => {
+ tiptapEditor = createTestEditor({
+ extensions: [
+ CodeBlockHighlight,
+ CodeSuggestion.configure({ config: { ...codeSuggestionConfig, ...config } }),
+ ],
+ });
+
+ ({
+ builders: { doc, codeSuggestion },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ codeSuggestion: { nodeType: CodeSuggestion.name },
+ },
+ }));
+ };
+
+ describe('insertCodeSuggestion command', () => {
+ it('creates a correct suggestion for a single line selection', async () => {
+ createEditor({ line: { new_line: 5 }, lines: [] });
+
+ await expectDocumentAfterTransaction({
+ number: 1,
+ tiptapEditor,
+ action: () => tiptapEditor.commands.insertCodeSuggestion(),
+ expectedDoc: doc(codeSuggestion({ langParams: '-0+0' }, '## Usage')),
+ });
+ });
+
+ it('creates a correct suggestion for a multi-line selection', async () => {
+ createEditor({
+ line: { new_line: 9 },
+ lines: [
+ { new_line: 5 },
+ { new_line: 6 },
+ { new_line: 7 },
+ { new_line: 8 },
+ { new_line: 9 },
+ ],
+ });
+
+ await expectDocumentAfterTransaction({
+ number: 1,
+ tiptapEditor,
+ action: () => tiptapEditor.commands.insertCodeSuggestion(),
+ expectedDoc: doc(
+ codeSuggestion({ langParams: '-4+0' }, '## Usage\n\n```yaml\nfoo: bar\n```'),
+ ),
+ });
+ });
+
+ it('does not insert a new suggestion if already inside a suggestion', async () => {
+ const initialDoc = codeSuggestion({ langParams: '-0+0' }, '## Usage');
+
+ createEditor({ line: { new_line: 5 }, lines: [] });
+
+ tiptapEditor.commands.setContent(doc(initialDoc).toJSON());
+
+ jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true);
+
+ tiptapEditor.commands.insertCodeSuggestion();
+ // wait some time to be sure no other transaction happened
+ await sleep();
+
+ expect(tiptapEditor.getJSON()).toEqual(doc(initialDoc).toJSON());
+ });
+ });
+
+ describe('when typing ```suggestion input rule', () => {
+ beforeEach(() => {
+ createEditor();
+
+ triggerNodeInputRule({
+ tiptapEditor,
+ inputRuleText: '```suggestion ',
+ });
+ });
+
+ it('creates a new code suggestion block with lines -0+0', () => {
+ const expectedDoc = doc(codeSuggestion({ language: 'suggestion', langParams: '-0+0' }));
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/comment_spec.js b/spec/frontend/content_editor/extensions/comment_spec.js
deleted file mode 100644
index 7d8ff28e4d7..00000000000
--- a/spec/frontend/content_editor/extensions/comment_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Comment from '~/content_editor/extensions/comment';
-import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
-
-describe('content_editor/extensions/comment', () => {
- let tiptapEditor;
- let doc;
- let comment;
-
- beforeEach(() => {
- tiptapEditor = createTestEditor({ extensions: [Comment] });
- ({
- builders: { doc, comment },
- } = createDocBuilder({
- tiptapEditor,
- names: {
- comment: { nodeType: Comment.name },
- },
- }));
- });
-
- describe('when typing the comment input rule', () => {
- it('inserts a comment node', () => {
- const expectedDoc = doc(comment());
-
- triggerNodeInputRule({ tiptapEditor, inputRuleText: '<!-- ' });
-
- expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
- });
- });
-});
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js
index baf0919fec8..f8faa7869c0 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js
@@ -1,5 +1,6 @@
-import PasteMarkdown from '~/content_editor/extensions/paste_markdown';
+import CopyPaste from '~/content_editor/extensions/copy_paste';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Loading from '~/content_editor/extensions/loading';
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
import Heading from '~/content_editor/extensions/heading';
@@ -10,29 +11,48 @@ import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
-import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
+import {
+ createTestEditor,
+ createDocBuilder,
+ waitUntilNextDocTransaction,
+ sleep,
+} from '../test_utils';
const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>';
+const CODE_SUGGESTION_HTML =
+ '<pre data-lang-params="-0+0" class="js-syntax-highlight language-suggestion" lang="suggestion">Suggested code</pre>';
const DIAGRAM_HTML =
'<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">';
const FRONTMATTER_HTML = '<pre lang="yaml" data-lang-params="frontmatter">key: value</pre>';
-const PARAGRAPH_HTML = '<p>Some text with <strong>bold</strong> and <em>italic</em> text.</p>';
+const PARAGRAPH_HTML =
+ '<p dir="auto">Some text with <strong>bold</strong> and <em>italic</em> text.</p>';
-describe('content_editor/extensions/paste_markdown', () => {
+describe('content_editor/extensions/copy_paste', () => {
let tiptapEditor;
let doc;
let p;
let bold;
let italic;
+ let loading;
let heading;
let codeBlock;
let renderMarkdown;
+ let resolveRenderMarkdownPromise;
+ let resolveRenderMarkdownPromiseAndWait;
+
let eventHub;
const defaultData = { 'text/plain': '**bold text**' };
beforeEach(() => {
- renderMarkdown = jest.fn();
eventHub = eventHubFactory();
+ renderMarkdown = jest.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveRenderMarkdownPromise = resolve;
+ resolveRenderMarkdownPromiseAndWait = (data) =>
+ waitUntilNextDocTransaction({ tiptapEditor, action: () => resolve(data) });
+ }),
+ );
jest.spyOn(eventHub, '$emit');
@@ -40,21 +60,23 @@ describe('content_editor/extensions/paste_markdown', () => {
extensions: [
Bold,
Italic,
+ Loading,
CodeBlockHighlight,
Diagram,
Frontmatter,
Heading,
- PasteMarkdown.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }),
+ CopyPaste.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }),
],
});
({
- builders: { doc, p, bold, italic, heading, codeBlock },
+ builders: { doc, p, bold, italic, heading, loading, codeBlock },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
italic: { markType: Italic.name },
+ loading: { nodeType: Loading.name },
heading: { nodeType: Heading.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
},
@@ -102,11 +124,12 @@ describe('content_editor/extensions/paste_markdown', () => {
});
it.each`
- nodeType | html | handled | desc
- ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'}
- ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'}
- ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'}
- ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'}
+ nodeType | html | handled | desc
+ ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'}
+ ${'codeSuggestion'} | ${CODE_SUGGESTION_HTML} | ${false} | ${'does not handle'}
+ ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'}
+ ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'}
+ ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'}
`('$desc paste if currently a `$nodeType` is in focus', async ({ html, handled }) => {
tiptapEditor.commands.insertContent(html);
@@ -153,15 +176,51 @@ describe('content_editor/extensions/paste_markdown', () => {
});
describe('when pasting raw markdown source', () => {
+ it('shows a loading indicator while markdown is being processed', async () => {
+ const expectedDoc = doc(p(loading({ id: expect.any(String) })));
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it('pastes in the correct position if some content is added before the markdown is processed', async () => {
+ const expectedDoc = doc(p(bold('some markdown'), 'some content'));
+ const resolvedValue = '<strong>some markdown</strong>';
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ tiptapEditor.commands.insertContent('some content');
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ expect(tiptapEditor.state.selection.from).toEqual(26); // end of the document
+ });
+
+ it('does not paste anything if the loading indicator is deleted before the markdown is processed', async () => {
+ const expectedDoc = doc(p());
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ tiptapEditor.chain().selectAll().deleteSelection().run();
+ resolveRenderMarkdownPromise('some markdown');
+
+ // wait some time to be sure no transaction happened
+ await sleep();
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+
describe('when rendering markdown succeeds', () => {
+ let resolvedValue;
+
beforeEach(() => {
- renderMarkdown.mockResolvedValueOnce('<strong>bold text</strong>');
+ resolvedValue = '<strong>bold text</strong>';
});
it('transforms pasted text into a prosemirror node', async () => {
const expectedDoc = doc(p(bold('bold text')));
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -173,6 +232,7 @@ describe('content_editor/extensions/paste_markdown', () => {
tiptapEditor.commands.setContent('Initial text and ');
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -186,6 +246,7 @@ describe('content_editor/extensions/paste_markdown', () => {
tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -193,8 +254,7 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting block content in an existing paragraph', () => {
beforeEach(() => {
- renderMarkdown.mockReset();
- renderMarkdown.mockResolvedValueOnce('<h1>Heading</h1><p><strong>bold text</strong></p>');
+ resolvedValue = '<h1>Heading</h1><p><strong>bold text</strong></p>';
});
it('inserts the block content after the existing paragraph', async () => {
@@ -207,6 +267,7 @@ describe('content_editor/extensions/paste_markdown', () => {
tiptapEditor.commands.setContent('Initial text and ');
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -215,9 +276,8 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting html content', () => {
it('strips out any stray div, pre, span tags', async () => {
- renderMarkdown.mockResolvedValueOnce(
- '<div><span dir="auto"><strong>bold text</strong></span></div><pre><code>some code</code></pre>',
- );
+ const resolvedValue =
+ '<div><span dir="auto"><strong>bold text</strong></span></div><pre><code>some code</code></pre>';
const expectedDoc = doc(p(bold('bold text')), p('some code'));
@@ -230,6 +290,7 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -237,8 +298,7 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting text/x-gfm', () => {
it('processes the content as markdown, even if html content exists', async () => {
- renderMarkdown.mockResolvedValueOnce('<strong>bold text</strong>');
-
+ const resolvedValue = '<strong>bold text</strong>';
const expectedDoc = doc(p(bold('bold text')));
await triggerPasteEventHandlerAndWaitForTransaction(
@@ -251,6 +311,7 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -258,9 +319,8 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting vscode-editor-data', () => {
it('pastes the content as a code block', async () => {
- renderMarkdown.mockResolvedValueOnce(
- '<div class="gl-relative markdown-code-block js-markdown-code">&#x000A;<pre data-sourcepos="1:1-3:3" data-canonical-lang="ruby" class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="nb">puts</span> <span class="s2">"Hello World"</span></span></code></pre>&#x000A;<copy-code></copy-code>&#x000A;</div>',
- );
+ const resolvedValue =
+ '<div class="gl-relative markdown-code-block js-markdown-code">&#x000A;<pre data-sourcepos="1:1-3:3" data-canonical-lang="ruby" class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="nb">puts</span> <span class="s2">"Hello World"</span></span></code></pre>&#x000A;<copy-code></copy-code>&#x000A;</div>';
const expectedDoc = doc(
codeBlock(
@@ -280,12 +340,13 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
it('pastes as regular markdown if language is markdown', async () => {
- renderMarkdown.mockResolvedValueOnce('<p><strong>bold text</strong></p>');
+ const resolvedValue = '<p><strong>bold text</strong></p>';
const expectedDoc = doc(p(bold('bold text')));
@@ -299,6 +360,7 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
diff --git a/spec/frontend/content_editor/extensions/hard_break_spec.js b/spec/frontend/content_editor/extensions/hard_break_spec.js
index 9e2e28b6e72..6a57e7eaa9b 100644
--- a/spec/frontend/content_editor/extensions/hard_break_spec.js
+++ b/spec/frontend/content_editor/extensions/hard_break_spec.js
@@ -3,35 +3,21 @@ import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/extensions/hard_break', () => {
let tiptapEditor;
- let eq;
+
let doc;
let p;
- let hardBreak;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [HardBreak] });
({
- builders: { doc, p, hardBreak },
- eq,
+ builders: { doc, p },
} = createDocBuilder({
tiptapEditor,
names: { hardBreak: { nodeType: HardBreak.name } },
}));
});
- describe('Shift-Enter shortcut', () => {
- it('inserts a hard break when shortcut is executed', () => {
- const initialDoc = doc(p(''));
- const expectedDoc = doc(p(hardBreak()));
-
- tiptapEditor.commands.setContent(initialDoc.toJSON());
- tiptapEditor.commands.keyboardShortcut('Shift-Enter');
-
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
- });
- });
-
describe('Mod-Enter shortcut', () => {
it('does not insert a hard break when shortcut is executed', () => {
const initialDoc = doc(p(''));
@@ -40,7 +26,7 @@ describe('content_editor/extensions/hard_break', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
tiptapEditor.commands.keyboardShortcut('Mod-Enter');
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
});
});
diff --git a/spec/frontend/content_editor/extensions/html_nodes_spec.js b/spec/frontend/content_editor/extensions/html_nodes_spec.js
index 24c68239025..3fe496aa708 100644
--- a/spec/frontend/content_editor/extensions/html_nodes_spec.js
+++ b/spec/frontend/content_editor/extensions/html_nodes_spec.js
@@ -28,9 +28,9 @@ describe('content_editor/extensions/html_nodes', () => {
});
it.each`
- input | insertedNodes
- ${'<div><p>foo</p></div>'} | ${() => div(p('foo'))}
- ${'<pre><p>foo</p></pre>'} | ${() => pre(p('foo'))}
+ input | insertedNodes
+ ${'<div><p dir="auto">foo</p></div>'} | ${() => div(p('foo'))}
+ ${'<pre><p dir="auto">foo</p></pre>'} | ${() => pre(p('foo'))}
`('parses and creates nodes for $input', ({ input, insertedNodes }) => {
const expectedDoc = doc(insertedNodes());
diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js
index f73b0143fd9..69f4f4c6d65 100644
--- a/spec/frontend/content_editor/extensions/image_spec.js
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -35,7 +35,7 @@ describe('content_editor/extensions/image', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
expect(tiptapEditor.getHTML()).toEqual(
- '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>',
+ '<p dir="auto"><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>',
);
});
});
diff --git a/spec/frontend/content_editor/extensions/paragraph_spec.js b/spec/frontend/content_editor/extensions/paragraph_spec.js
new file mode 100644
index 00000000000..d04dda1871d
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/paragraph_spec.js
@@ -0,0 +1,29 @@
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/paragraph', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor();
+
+ ({
+ builders: { doc, p },
+ } = createDocBuilder({ tiptapEditor }));
+ });
+
+ describe('Shift-Enter shortcut', () => {
+ it('inserts a new paragraph when shortcut is executed', async () => {
+ const initialDoc = doc(p('hello'));
+ const expectedDoc = doc(p('hello'), p(''));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.keyboardShortcut('Shift-Enter');
+
+ await Promise.resolve();
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 927a7d59899..3d4d5b13120 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -1337,13 +1337,13 @@ content
alert("Hello world")
</script>
`,
- expectedHtml: '<p></p>',
+ expectedHtml: '<p dir="auto"></p>',
},
{
markdown: `
<foo>Hello</foo>
`,
- expectedHtml: '<p></p>',
+ expectedHtml: '<p dir="auto"></p>',
},
{
markdown: `
@@ -1356,7 +1356,7 @@ alert("Hello world")
<a id="link-id">Header</a> and other text
`,
expectedHtml:
- '<p><a target="_blank" rel="noopener noreferrer nofollow">Header</a> and other text</p>',
+ '<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow">Header</a> and other text</p>',
},
{
markdown: `
@@ -1366,11 +1366,11 @@ body {
}
</style>
`,
- expectedHtml: '<p></p>',
+ expectedHtml: '<p dir="auto"></p>',
},
{
markdown: '<div style="transform">div</div>',
- expectedHtml: '<div><p>div</p></div>',
+ expectedHtml: '<div><p dir="auto">div</p></div>',
},
])(
'removes unknown tags and unsupported attributes from HTML output',
@@ -1421,6 +1421,7 @@ body {
};
};
+ // NOTE: unicode \u001 and \u003 cannot be used in test names because they cause test report XML parsing errors
it.each`
desc | urlInput | urlOutput
${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
@@ -1439,7 +1440,7 @@ body {
${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"}
${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
- `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
+ `('sanitize $desc becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
const exampleFactories = [docWithImageFactory, docWithLinkFactory];
exampleFactories.forEach(async (exampleFactory) => {
diff --git a/spec/frontend/content_editor/services/code_suggestion_utils_spec.js b/spec/frontend/content_editor/services/code_suggestion_utils_spec.js
new file mode 100644
index 00000000000..f26d33adf4c
--- /dev/null
+++ b/spec/frontend/content_editor/services/code_suggestion_utils_spec.js
@@ -0,0 +1,53 @@
+import {
+ lineOffsetToLangParams,
+ langParamsToLineOffset,
+ toAbsoluteLineOffset,
+ getLines,
+ appendNewlines,
+} from '~/content_editor/services/code_suggestion_utils';
+
+describe('content_editor/services/code_suggestion_utils', () => {
+ describe('lineOffsetToLangParams', () => {
+ it.each`
+ lineOffset | expected
+ ${[0, 0]} | ${'-0+0'}
+ ${[0, 2]} | ${'-0+2'}
+ ${[1, 1]} | ${'+1+1'}
+ ${[-1, 1]} | ${'-1+1'}
+ `('converts line offset $lineOffset to lang params $expected', ({ lineOffset, expected }) => {
+ expect(lineOffsetToLangParams(lineOffset)).toBe(expected);
+ });
+ });
+
+ describe('langParamsToLineOffset', () => {
+ it.each`
+ langParams | expected
+ ${'-0+0'} | ${[-0, 0]}
+ ${'-0+2'} | ${[-0, 2]}
+ ${'+1+1'} | ${[1, 1]}
+ ${'-1+1'} | ${[-1, 1]}
+ `('converts lang params $langParams to line offset $expected', ({ langParams, expected }) => {
+ expect(langParamsToLineOffset(langParams)).toEqual(expected);
+ });
+ });
+
+ describe('toAbsoluteLineOffset', () => {
+ it('adds line number to line offset', () => {
+ expect(toAbsoluteLineOffset([-2, 3], 72)).toEqual([70, 75]);
+ });
+ });
+
+ describe('getLines', () => {
+ it('returns lines from allLines', () => {
+ const allLines = ['foo', 'bar', 'baz', 'qux', 'quux'];
+ expect(getLines([2, 4], allLines)).toEqual(['bar', 'baz', 'qux']);
+ });
+ });
+
+ describe('appendNewlines', () => {
+ it('appends zero-width space to each line', () => {
+ const lines = ['foo', 'bar', 'baz'];
+ expect(appendNewlines(lines)).toEqual(['foo\u200b\n', 'bar\u200b\n', 'baz\u200b']);
+ });
+ });
+});
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 b9a9c3ccd17..b68d57971b9 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -46,14 +46,6 @@ describe('content_editor/services/create_content_editor', () => {
});
});
- it('sets gl-shadow-none! class selector to the tiptapEditor instance', () => {
- expect(editor.tiptapEditor.options.editorProps).toMatchObject({
- attributes: {
- class: 'gl-shadow-none!',
- },
- });
- });
-
it('allows providing external content editor extensions', () => {
const labelReference = 'this is a ~group::editor';
const { tiptapExtension, serializer } = createTestContentEditorExtension();
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 a9960918e62..1f7b56ef762 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
@@ -1,6 +1,5 @@
import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
-import Comment from '~/content_editor/extensions/comment';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/gl_api_markdown_deserializer', () => {
@@ -8,21 +7,19 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
let doc;
let p;
let bold;
- let comment;
let tiptapEditor;
beforeEach(() => {
tiptapEditor = createTestEditor({
- extensions: [Bold, Comment],
+ extensions: [Bold],
});
({
- builders: { doc, p, bold, comment },
+ builders: { doc, p, bold },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
- comment: { nodeType: Comment.name },
},
}));
renderMarkdown = jest.fn();
@@ -35,16 +32,16 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p><!-- some comment -->`);
+ renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`);
result = await deserializer.deserialize({
- markdown: '**Bold text**\n<!-- some comment -->',
+ markdown: '**Bold text**',
schema: tiptapEditor.schema,
});
});
it('transforms HTML returned by render function to a ProseMirror document', () => {
- const document = doc(p(bold(text)), comment(' some comment '));
+ const document = doc(p(bold(text)));
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 4521822042c..7be8114902a 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -3,7 +3,6 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import Comment from '~/content_editor/extensions/comment';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
@@ -56,7 +55,6 @@ const {
bulletList,
code,
codeBlock,
- comment,
details,
detailsContent,
div,
@@ -99,7 +97,6 @@ const {
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
- comment: { nodeType: Comment.name },
details: { nodeType: Details.name },
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
@@ -187,30 +184,6 @@ describe('markdownSerializer', () => {
);
});
- it('correctly serializes a comment node', () => {
- expect(serialize(paragraph('hi'), comment(' this is a\ncomment '))).toBe(
- `
-hi
-
-<!-- this is a
-comment -->
- `.trim(),
- );
- });
-
- it('correctly renders a comment with markdown in it without adding any slashes', () => {
- expect(serialize(paragraph('hi'), comment('this is a list\n- a\n- b\n- c'))).toBe(
- `
-hi
-
-<!--this is a list
-- a
-- b
-- c-->
- `.trim(),
- );
- });
-
it('escapes < and > in a paragraph', () => {
expect(
serialize(paragraph(text("some prose: <this> and </this> looks like code, but isn't"))),
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 2184a829cf0..f1c9fd47eb7 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -1,7 +1,4 @@
import { Node } from '@tiptap/core';
-import { Document } from '@tiptap/extension-document';
-import { Paragraph } from '@tiptap/extension-paragraph';
-import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
@@ -12,12 +9,12 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import Comment from '~/content_editor/extensions/comment';
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 Diagram from '~/content_editor/extensions/diagram';
+import Document from '~/content_editor/extensions/document';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
@@ -36,6 +33,7 @@ import Italic from '~/content_editor/extensions/italic';
import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
import OrderedList from '~/content_editor/extensions/ordered_list';
+import Paragraph from '~/content_editor/extensions/paragraph';
import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Reference from '~/content_editor/extensions/reference';
import ReferenceLabel from '~/content_editor/extensions/reference_label';
@@ -47,10 +45,13 @@ import TableRow from '~/content_editor/extensions/table_row';
import TableOfContents from '~/content_editor/extensions/table_of_contents';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
+import Text from '~/content_editor/extensions/text';
import Video from '~/content_editor/extensions/video';
import HTMLMarks from '~/content_editor/extensions/html_marks';
import HTMLNodes from '~/content_editor/extensions/html_nodes';
+export const DEFAULT_WAIT_TIMEOUT = 100;
+
export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
const docBuilders = builders(tiptapEditor.schema, {
p: { nodeType: 'paragraph' },
@@ -239,6 +240,16 @@ export const waitUntilTransaction = ({ tiptapEditor, number, action }) => {
});
};
+export const sleep = (time = DEFAULT_WAIT_TIMEOUT) => {
+ jest.useRealTimers();
+ const promise = new Promise((resolve) => {
+ setTimeout(resolve, time);
+ });
+ jest.useFakeTimers();
+
+ return promise;
+};
+
export const expectDocumentAfterTransaction = ({ tiptapEditor, number, expectedDoc, action }) => {
return new Promise((resolve) => {
let counter = 0;
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
index 6672d3eb18b..5bce0ca3746 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
@@ -1,21 +1,18 @@
-import events from 'test_fixtures/controller/users/activity.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventApproved from '~/contribution_events/components/contribution_event/contribution_event_approved.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
-import TargetLink from '~/contribution_events/components/target_link.vue';
-import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import { eventApproved } from '../../utils';
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+const defaultPropsData = {
+ event: eventApproved(),
+};
describe('ContributionEventApproved', () => {
let wrapper;
const createComponent = () => {
- wrapper = mountExtended(ContributionEventApproved, {
- propsData: {
- event: eventApproved,
- },
+ wrapper = shallowMountExtended(ContributionEventApproved, {
+ propsData: defaultPropsData,
});
};
@@ -25,23 +22,10 @@ describe('ContributionEventApproved', () => {
it('renders `ContributionEventBase`', () => {
expect(wrapper.findComponent(ContributionEventBase).props()).toEqual({
- event: eventApproved,
+ event: defaultPropsData.event,
iconName: 'approval-solid',
iconClass: 'gl-text-green-500',
+ message: ContributionEventApproved.i18n.message,
});
});
-
- it('renders message', () => {
- expect(wrapper.findByTestId('event-body').text()).toBe(
- `Approved merge request ${eventApproved.target.reference_link_text} in ${eventApproved.resource_parent.full_name}.`,
- );
- });
-
- it('renders target link', () => {
- expect(wrapper.findComponent(TargetLink).props('event')).toEqual(eventApproved);
- });
-
- it('renders resource parent link', () => {
- expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(eventApproved);
- });
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
index 8c951e20bed..310966243d1 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
@@ -1,23 +1,27 @@
import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
-const [event] = events;
+import TargetLink from '~/contribution_events/components/target_link.vue';
+import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import { eventApproved } from '../../utils';
describe('ContributionEventBase', () => {
let wrapper;
const defaultPropsData = {
- event,
+ event: eventApproved(),
iconName: 'approval-solid',
iconClass: 'gl-text-green-500',
+ message: 'Approved merge request %{targetLink} in %{resourceParentLink}.',
};
- const createComponent = () => {
- wrapper = shallowMountExtended(ContributionEventBase, {
- propsData: defaultPropsData,
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(ContributionEventBase, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
scopedSlots: {
default: '<div data-testid="default-slot"></div>',
'additional-info': '<div data-testid="additional-info-slot"></div>',
@@ -25,38 +29,75 @@ describe('ContributionEventBase', () => {
});
};
- beforeEach(() => {
+ it('renders avatar', () => {
createComponent();
- });
- it('renders avatar', () => {
const avatarLink = wrapper.findComponent(GlAvatarLink);
+ const avatarLabeled = avatarLink.findComponent(GlAvatarLabeled);
- expect(avatarLink.attributes('href')).toBe(event.author.web_url);
- expect(avatarLink.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
- label: event.author.name,
- sublabel: `@${event.author.username}`,
- src: event.author.avatar_url,
+ expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url);
+ expect(avatarLabeled.attributes()).toMatchObject({
+ src: defaultPropsData.event.author.avatar_url,
size: '32',
});
+ expect(avatarLabeled.props()).toMatchObject({
+ label: defaultPropsData.event.author.name,
+ subLabel: `@${defaultPropsData.event.author.username}`,
+ });
});
it('renders time ago tooltip', () => {
- expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(event.created_at);
+ createComponent();
+
+ expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(
+ defaultPropsData.event.created_at,
+ );
});
it('renders icon', () => {
+ createComponent();
+
const icon = wrapper.findComponent(GlIcon);
expect(icon.props('name')).toBe(defaultPropsData.iconName);
expect(icon.classes()).toContain(defaultPropsData.iconClass);
});
- it('renders `default` slot', () => {
- expect(wrapper.findByTestId('default-slot').exists()).toBe(true);
+ describe('when `message` prop is passed', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toBe(
+ `Approved merge request ${defaultPropsData.event.target.reference_link_text} in ${defaultPropsData.event.resource_parent.full_name}.`,
+ );
+ });
+
+ it('renders target link', () => {
+ expect(wrapper.findComponent(TargetLink).props('event')).toEqual(defaultPropsData.event);
+ });
+
+ it('renders resource parent link', () => {
+ expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
+ defaultPropsData.event,
+ );
+ });
+ });
+
+ describe('when `message` prop is not passed', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { message: '' } });
+ });
+
+ it('renders `default` slot', () => {
+ expect(wrapper.findByTestId('default-slot').exists()).toBe(true);
+ });
});
it('renders `additional-info` slot', () => {
+ createComponent();
+
expect(wrapper.findByTestId('additional-info-slot').exists()).toBe(true);
});
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
new file mode 100644
index 00000000000..c58fca1ad12
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventExpired from '~/contribution_events/components/contribution_event/contribution_event_expired.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventExpired } from '../../utils';
+
+const defaultPropsData = {
+ event: eventExpired(),
+};
+
+describe('ContributionEventExpired', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventExpired, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'expire',
+ message: ContributionEventExpired.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
new file mode 100644
index 00000000000..56688e2ef27
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventJoined from '~/contribution_events/components/contribution_event/contribution_event_joined.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventJoined } from '../../utils';
+
+const defaultPropsData = {
+ event: eventJoined(),
+};
+
+describe('ContributionEventJoined', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventJoined, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'users',
+ message: ContributionEventJoined.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
new file mode 100644
index 00000000000..58cb8714d03
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventLeft from '~/contribution_events/components/contribution_event/contribution_event_left.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventLeft } from '../../utils';
+
+const defaultPropsData = {
+ event: eventLeft(),
+};
+
+describe('ContributionEventLeft', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventLeft, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'leave',
+ message: ContributionEventLeft.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js
new file mode 100644
index 00000000000..88494c24ddf
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js
@@ -0,0 +1,31 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventMerged from '~/contribution_events/components/contribution_event/contribution_event_merged.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventMerged } from '../../utils';
+
+const defaultPropsData = {
+ event: eventMerged(),
+};
+
+describe('ContributionEventMerged', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventMerged, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toEqual({
+ event: defaultPropsData.event,
+ iconName: 'git-merge',
+ iconClass: 'gl-text-blue-600',
+ message: ContributionEventMerged.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js
new file mode 100644
index 00000000000..42855134a09
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js
@@ -0,0 +1,33 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventPrivate from '~/contribution_events/components/contribution_event/contribution_event_private.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventPrivate } from '../../utils';
+
+const defaultPropsData = {
+ event: eventPrivate(),
+};
+
+describe('ContributionEventPrivate', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mountExtended(ContributionEventPrivate, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'eye-slash',
+ });
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toBe(ContributionEventPrivate.i18n.message);
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js
new file mode 100644
index 00000000000..43f201040e3
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js
@@ -0,0 +1,141 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventPushed from '~/contribution_events/components/contribution_event/contribution_event_pushed.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import {
+ eventPushedNewBranch,
+ eventPushedNewTag,
+ eventPushedBranch,
+ eventPushedTag,
+ eventPushedRemovedBranch,
+ eventPushedRemovedTag,
+ eventBulkPushedBranch,
+} from '../../utils';
+
+describe('ContributionEventPushed', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData }) => {
+ wrapper = mountExtended(ContributionEventPushed, {
+ propsData,
+ });
+ };
+
+ describe.each`
+ event | expectedMessage | expectedIcon
+ ${eventPushedNewBranch()} | ${'Pushed a new branch'} | ${'commit'}
+ ${eventPushedNewTag()} | ${'Pushed a new tag'} | ${'commit'}
+ ${eventPushedBranch()} | ${'Pushed to branch'} | ${'commit'}
+ ${eventPushedTag()} | ${'Pushed to tag'} | ${'commit'}
+ ${eventPushedRemovedBranch()} | ${'Deleted branch'} | ${'remove'}
+ ${eventPushedRemovedTag()} | ${'Deleted tag'} | ${'remove'}
+ `('when event is $event', ({ event, expectedMessage, expectedIcon }) => {
+ beforeEach(() => {
+ createComponent({ propsData: { event } });
+ });
+
+ it('renders `ContributionEventBase` with correct props', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event,
+ iconName: expectedIcon,
+ });
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toContain(expectedMessage);
+ });
+
+ it('renders resource parent link', () => {
+ expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(event);
+ });
+ });
+
+ describe('when ref has a path', () => {
+ const event = eventPushedNewBranch();
+ const path = '/foo';
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event: {
+ ...event,
+ ref: {
+ ...event.ref,
+ path,
+ },
+ },
+ },
+ });
+ });
+
+ it('renders ref link', () => {
+ expect(wrapper.findByRole('link', { name: event.ref.name }).attributes('href')).toBe(path);
+ });
+ });
+
+ describe('when ref does not have a path', () => {
+ const event = eventPushedRemovedBranch();
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+ });
+
+ it('renders ref name without a link', () => {
+ expect(wrapper.findByRole('link', { name: event.ref.name }).exists()).toBe(false);
+ expect(wrapper.findByText(event.ref.name).exists()).toBe(true);
+ });
+ });
+
+ it('renders renders a link to the commit', () => {
+ const event = eventPushedNewBranch();
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+
+ expect(
+ wrapper.findByRole('link', { name: event.commit.truncated_sha }).attributes('href'),
+ ).toBe(event.commit.path);
+ });
+
+ it('renders commit title', () => {
+ const event = eventPushedNewBranch();
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+
+ expect(wrapper.findByText(event.commit.title).exists()).toBe(true);
+ });
+
+ describe('when multiple commits are pushed', () => {
+ const event = eventBulkPushedBranch();
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+ });
+
+ it('renders message', () => {
+ expect(wrapper.text()).toContain('…and 4 more commits.');
+ });
+
+ it('renders compare link', () => {
+ expect(
+ wrapper
+ .findByRole('link', {
+ name: `Compare ${event.commit.from_truncated_sha}…${event.commit.to_truncated_sha}`,
+ })
+ .attributes('href'),
+ ).toBe(event.commit.compare_path);
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_events_spec.js b/spec/frontend/contribution_events/components/contribution_events_spec.js
index 4bc354c393f..31e1bc3e569 100644
--- a/spec/frontend/contribution_events/components/contribution_events_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_events_spec.js
@@ -1,10 +1,21 @@
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import ContributionEvents from '~/contribution_events/components/contribution_events.vue';
import ContributionEventApproved from '~/contribution_events/components/contribution_event/contribution_event_approved.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import ContributionEventExpired from '~/contribution_events/components/contribution_event/contribution_event_expired.vue';
+import ContributionEventJoined from '~/contribution_events/components/contribution_event/contribution_event_joined.vue';
+import ContributionEventLeft from '~/contribution_events/components/contribution_event/contribution_event_left.vue';
+import ContributionEventPushed from '~/contribution_events/components/contribution_event/contribution_event_pushed.vue';
+import ContributionEventPrivate from '~/contribution_events/components/contribution_event/contribution_event_private.vue';
+import ContributionEventMerged from '~/contribution_events/components/contribution_event/contribution_event_merged.vue';
+import {
+ eventApproved,
+ eventExpired,
+ eventJoined,
+ eventLeft,
+ eventPushedBranch,
+ eventPrivate,
+ eventMerged,
+} from '../utils';
describe('ContributionEvents', () => {
let wrapper;
@@ -12,14 +23,28 @@ describe('ContributionEvents', () => {
const createComponent = () => {
wrapper = shallowMountExtended(ContributionEvents, {
propsData: {
- events,
+ events: [
+ eventApproved(),
+ eventExpired(),
+ eventJoined(),
+ eventLeft(),
+ eventPushedBranch(),
+ eventPrivate(),
+ eventMerged(),
+ ],
},
});
};
it.each`
expectedComponent | expectedEvent
- ${ContributionEventApproved} | ${eventApproved}
+ ${ContributionEventApproved} | ${eventApproved()}
+ ${ContributionEventExpired} | ${eventExpired()}
+ ${ContributionEventJoined} | ${eventJoined()}
+ ${ContributionEventLeft} | ${eventLeft()}
+ ${ContributionEventPushed} | ${eventPushedBranch()}
+ ${ContributionEventPrivate} | ${eventPrivate()}
+ ${ContributionEventMerged} | ${eventMerged()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {
diff --git a/spec/frontend/contribution_events/components/resource_parent_link_spec.js b/spec/frontend/contribution_events/components/resource_parent_link_spec.js
index 8d586db2a30..815a1b751cf 100644
--- a/spec/frontend/contribution_events/components/resource_parent_link_spec.js
+++ b/spec/frontend/contribution_events/components/resource_parent_link_spec.js
@@ -1,30 +1,52 @@
import { GlLink } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import { EVENT_TYPE_PRIVATE } from '~/contribution_events/constants';
+import { eventApproved } from '../utils';
describe('ResourceParentLink', () => {
let wrapper;
- const createComponent = () => {
+ const defaultPropsData = {
+ event: eventApproved(),
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(ResourceParentLink, {
propsData: {
- event: eventApproved,
+ ...defaultPropsData,
+ ...propsData,
},
});
};
- beforeEach(() => {
- createComponent();
+ describe('when resource parent is defined', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders link', () => {
+ const link = wrapper.findComponent(GlLink);
+ const { web_url, full_name } = defaultPropsData.event.resource_parent;
+
+ expect(link.attributes('href')).toBe(web_url);
+ expect(link.text()).toBe(full_name);
+ });
});
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
+ describe('when resource parent is not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event: {
+ type: EVENT_TYPE_PRIVATE,
+ },
+ },
+ });
+ });
- expect(link.attributes('href')).toBe(eventApproved.resource_parent.web_url);
- expect(link.text()).toBe(eventApproved.resource_parent.full_name);
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
});
diff --git a/spec/frontend/contribution_events/components/target_link_spec.js b/spec/frontend/contribution_events/components/target_link_spec.js
index 7944375487b..b71d6eff432 100644
--- a/spec/frontend/contribution_events/components/target_link_spec.js
+++ b/spec/frontend/contribution_events/components/target_link_spec.js
@@ -1,33 +1,48 @@
import { GlLink } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import TargetLink from '~/contribution_events/components/target_link.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import { eventApproved, eventJoined } from '../utils';
describe('TargetLink', () => {
let wrapper;
- const createComponent = () => {
+ const defaultPropsData = {
+ event: eventApproved(),
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(TargetLink, {
propsData: {
- event: eventApproved,
+ ...defaultPropsData,
+ ...propsData,
},
});
};
- beforeEach(() => {
- createComponent();
+ describe('when target is defined', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders link', () => {
+ const link = wrapper.findComponent(GlLink);
+ const { web_url: webUrl, title, reference_link_text } = defaultPropsData.event.target;
+
+ expect(link.attributes()).toMatchObject({
+ href: webUrl,
+ title,
+ });
+ expect(link.text()).toBe(reference_link_text);
+ });
});
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
+ describe('when target is not defined', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { event: eventJoined() } });
+ });
- expect(link.attributes()).toMatchObject({
- href: eventApproved.target.web_url,
- title: eventApproved.target.title,
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
});
- expect(link.text()).toBe(eventApproved.target.reference_link_text);
});
});
diff --git a/spec/frontend/contribution_events/utils.js b/spec/frontend/contribution_events/utils.js
new file mode 100644
index 00000000000..6e97455582d
--- /dev/null
+++ b/spec/frontend/contribution_events/utils.js
@@ -0,0 +1,52 @@
+import events from 'test_fixtures/controller/users/activity.json';
+import {
+ EVENT_TYPE_APPROVED,
+ EVENT_TYPE_EXPIRED,
+ EVENT_TYPE_JOINED,
+ EVENT_TYPE_LEFT,
+ EVENT_TYPE_PUSHED,
+ EVENT_TYPE_PRIVATE,
+ EVENT_TYPE_MERGED,
+ PUSH_EVENT_REF_TYPE_BRANCH,
+ PUSH_EVENT_REF_TYPE_TAG,
+} from '~/contribution_events/constants';
+
+const findEventByAction = (action) => events.find((event) => event.action === action);
+
+export const eventApproved = () => findEventByAction(EVENT_TYPE_APPROVED);
+
+export const eventExpired = () => findEventByAction(EVENT_TYPE_EXPIRED);
+
+export const eventJoined = () => findEventByAction(EVENT_TYPE_JOINED);
+
+export const eventLeft = () => findEventByAction(EVENT_TYPE_LEFT);
+
+export const eventMerged = () => findEventByAction(EVENT_TYPE_MERGED);
+
+const findPushEvent = ({
+ isNew = false,
+ isRemoved = false,
+ refType = PUSH_EVENT_REF_TYPE_BRANCH,
+ commitCount = 1,
+} = {}) => () =>
+ events.find(
+ ({ action, ref, commit }) =>
+ action === EVENT_TYPE_PUSHED &&
+ ref.is_new === isNew &&
+ ref.is_removed === isRemoved &&
+ ref.type === refType &&
+ commit.count === commitCount,
+ );
+
+export const eventPushedNewBranch = findPushEvent({ isNew: true });
+export const eventPushedNewTag = findPushEvent({ isNew: true, refType: PUSH_EVENT_REF_TYPE_TAG });
+export const eventPushedBranch = findPushEvent();
+export const eventPushedTag = findPushEvent({ refType: PUSH_EVENT_REF_TYPE_TAG });
+export const eventPushedRemovedBranch = findPushEvent({ isRemoved: true });
+export const eventPushedRemovedTag = findPushEvent({
+ isRemoved: true,
+ refType: PUSH_EVENT_REF_TYPE_TAG,
+});
+export const eventBulkPushedBranch = findPushEvent({ commitCount: 5 });
+
+export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE });
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index 3dfb828b449..de4112134ce 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import deployKeysApp from '~/deploy_keys/components/app.vue';
import ConfirmModal from '~/deploy_keys/components/confirm_modal.vue';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '~/deploy_keys/eventhub';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -39,6 +40,7 @@ describe('Deploy keys app component', () => {
const findLoadingIcon = () => wrapper.find('.gl-spinner');
const findKeyPanels = () => wrapper.findAll('.deploy-keys .gl-tabs-nav li');
const findModal = () => wrapper.findComponent(ConfirmModal);
+ const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
it('renders loading icon while waiting for request', async () => {
mock.onGet(TEST_ENDPOINT).reply(() => new Promise());
@@ -74,55 +76,61 @@ describe('Deploy keys app component', () => {
});
});
- it('re-fetches deploy keys when enabling a key', async () => {
- const key = data.public_keys[0];
+ it('hasKeys returns true when there are keys', async () => {
await mountComponent();
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve());
- eventHub.$emit('enable.key', key);
-
- await nextTick();
- expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
});
- it('re-fetches deploy keys when disabling a key', async () => {
+ describe('enabling and disabling keys', () => {
const key = data.public_keys[0];
- await mountComponent();
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+ let getMethodMock;
+ let putMethodMock;
- eventHub.$emit('disable.key', key, () => {});
+ const removeKey = async (keyEvent) => {
+ eventHub.$emit(keyEvent, key, () => {});
- await nextTick();
- expect(findModal().props('visible')).toBe(true);
- findModal().vm.$emit('remove');
+ await nextTick();
+ expect(findModal().props('visible')).toBe(true);
+ findModal().vm.$emit('remove');
+ };
- await nextTick();
- expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ beforeEach(() => {
+ getMethodMock = jest.spyOn(axios, 'get');
+ putMethodMock = jest.spyOn(axios, 'put');
+ });
- it('calls disableKey when removing a key', async () => {
- const key = data.public_keys[0];
- await mountComponent();
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+ afterEach(() => {
+ getMethodMock.mockClear();
+ putMethodMock.mockClear();
+ });
- eventHub.$emit('remove.key', key, () => {});
+ it('re-fetches deploy keys when enabling a key', async () => {
+ await mountComponent();
- await nextTick();
- expect(findModal().props('visible')).toBe(true);
- findModal().vm.$emit('remove');
+ eventHub.$emit('enable.key', key);
- await nextTick();
- expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ expect(putMethodMock).toHaveBeenCalledWith(`${TEST_ENDPOINT}/${key.id}/enable`);
+ expect(getMethodMock).toHaveBeenCalled();
+ });
- it('hasKeys returns true when there are keys', async () => {
- await mountComponent();
- expect(wrapper.vm.hasKeys).toEqual(3);
+ it('re-fetches deploy keys when disabling a key', async () => {
+ await mountComponent();
+
+ await removeKey('disable.key');
+
+ expect(putMethodMock).toHaveBeenCalledWith(`${TEST_ENDPOINT}/${key.id}/disable`);
+ expect(getMethodMock).toHaveBeenCalled();
+ });
+
+ it('calls disableKey when removing a key', async () => {
+ await mountComponent();
+
+ await removeKey('remove.key');
+
+ expect(putMethodMock).toHaveBeenCalledWith(`${TEST_ENDPOINT}/${key.id}/disable`);
+ expect(getMethodMock).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/design_management/components/design_description/description_form_spec.js b/spec/frontend/design_management/components/design_description/description_form_spec.js
index 8c01023b1a8..a61cc2af9b6 100644
--- a/spec/frontend/design_management/components/design_description/description_form_spec.js
+++ b/spec/frontend/design_management/components/design_description/description_form_spec.js
@@ -1,18 +1,15 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-
import { GlAlert } from '@gitlab/ui';
-
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-
import DescriptionForm from '~/design_management/components/design_description/description_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import updateDesignDescriptionMutation from '~/design_management/graphql/mutations/update_design_description.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-
+import { mockTracking } from 'helpers/tracking_helper';
import { designFactory, designUpdateFactory } from '../../mock_data/apollo_mock';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -86,6 +83,8 @@ describe('Design description form', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
describe('user has updateDesign permission', () => {
+ let trackingSpy;
+
const ctrlKey = {
ctrlKey: true,
};
@@ -96,6 +95,8 @@ describe('Design description form', () => {
const errorMessage = 'Could not update description. Please try again.';
beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
+
createComponent();
});
@@ -139,19 +140,19 @@ describe('Design description form', () => {
mockDesign.id,
)}`,
markdownDocsPath: '/help/user/markdown',
- quickActionsDocsPath: '/help/user/project/quick_actions',
});
});
- it.each`
+ describe.each`
isKeyEvent | assertionName | key | keyData
${true} | ${'Ctrl + Enter keypress'} | ${'ctrl'} | ${ctrlKey}
${true} | ${'Meta + Enter keypress'} | ${'meta'} | ${metaKey}
${false} | ${'Save button click'} | ${''} | ${null}
- `(
- 'hides form and calls mutation when form is submitted via $assertionName',
- async ({ isKeyEvent, keyData }) => {
- const mockDesignUpdateResponseHandler = jest.fn().mockResolvedValue(
+ `('when form is submitted via $assertionName', ({ isKeyEvent, keyData }) => {
+ let mockDesignUpdateResponseHandler;
+
+ beforeEach(async () => {
+ mockDesignUpdateResponseHandler = jest.fn().mockResolvedValue(
designUpdateFactory({
description: mockDescription,
descriptionHtml: `<p data-sourcepos="1:1-1:16" dir="auto">${mockDescription}</p>`,
@@ -171,7 +172,9 @@ describe('Design description form', () => {
}
await nextTick();
+ });
+ it('hides form and calls mutation', async () => {
expect(mockDesignUpdateResponseHandler).toHaveBeenCalledWith({
input: {
description: 'Hello world',
@@ -182,8 +185,16 @@ describe('Design description form', () => {
await waitForPromises();
expect(findMarkdownEditor().exists()).toBe(false);
- },
- );
+ });
+
+ it('tracks submit action', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Design',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
+ });
it('shows error message when mutation fails', async () => {
const failureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
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
deleted file mode 100644
index 9bb85ecf569..00000000000
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ /dev/null
@@ -1,86 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design note component should match the snapshot 1`] = `
-<timelineentryitem-stub
- class="design-note note-form"
- id="note_123"
->
- <glavatarlink-stub
- class="gl-float-left gl-mr-3"
- href="https://gitlab.com/user"
- >
- <glavatar-stub
- alt="avatar"
- entityid="0"
- entityname="foo-bar"
- shape="circle"
- size="32"
- src="https://gitlab.com/avatar"
- />
- </glavatarlink-stub>
-
- <div
- class="gl-display-flex gl-justify-content-space-between"
- >
- <div>
- <gllink-stub
- class="js-user-link"
- data-testid="user-link"
- data-user-id="1"
- data-username="foo-bar"
- href="https://gitlab.com/user"
- >
- <span
- class="note-header-author-name gl-font-weight-bold"
- >
-
- </span>
-
- <!---->
-
- <span
- class="note-headline-light"
- >
- @foo-bar
- </span>
- </gllink-stub>
-
- <span
- class="note-headline-light note-headline-meta"
- >
- <span
- class="system-note-message"
- />
-
- <gllink-stub
- class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm"
- href="#note_123"
- >
- <timeagotooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2019-07-26T15:02:20Z"
- tooltipplacement="bottom"
- />
- </gllink-stub>
- </span>
- </div>
-
- <div
- class="gl-display-flex gl-align-items-baseline gl-mt-n2 gl-mr-n2"
- >
-
- <!---->
-
- <!---->
- </div>
- </div>
-
- <div
- class="note-text md"
- data-qa-selector="note_content"
- data-testid="note-text"
- />
-
-</timelineentryitem-stub>
-`;
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 664a0974549..797f399eff5 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
@@ -29,6 +29,7 @@ const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
let wrapper;
+ const findDesignNotesList = () => wrapper.find('[data-testid="design-discussion-content"]');
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder);
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
@@ -88,6 +89,9 @@ describe('Design discussions component', () => {
},
},
},
+ stubs: {
+ EmojiPicker: true,
+ },
});
}
@@ -287,7 +291,7 @@ describe('Design discussions component', () => {
describe('when any note from a discussion is active', () => {
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
- 'applies correct class to all notes in the active discussion',
+ 'applies correct class to the active discussion',
(note) => {
createComponent({
props: { discussion: mockDiscussion },
@@ -299,11 +303,7 @@ describe('Design discussions component', () => {
},
});
- expect(
- wrapper
- .findAllComponents(DesignNote)
- .wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')),
- ).toBe(true);
+ expect(findDesignNotesList().classes('gl-bg-blue-50')).toBe(true);
},
);
});
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 661d1ac4087..8795b089551 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,10 +1,18 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import * as Sentry from '@sentry/browser';
+
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import EmojiPicker from '~/emoji/components/picker.vue';
+import DesignNoteAwardsList from '~/design_management/components/design_notes/design_note_awards_list.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import designNoteAwardEmojiToggleMutation from '~/design_management/graphql/mutations/design_note_award_emoji_toggle.mutation.graphql';
+import { mockAwardEmoji } from '../../mock_data/apollo_mock';
const scrollIntoViewMock = jest.fn();
const note = {
@@ -15,9 +23,11 @@ const note = {
avatarUrl: 'https://gitlab.com/avatar',
webUrl: 'https://gitlab.com/user',
},
+ awardEmoji: mockAwardEmoji,
body: 'test',
userPermissions: {
adminNote: false,
+ awardEmoji: true,
},
createdAt: '2019-07-26T15:02:20Z',
};
@@ -27,14 +37,14 @@ const $route = {
hash: '#note_123',
};
-const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } });
-
describe('Design note component', () => {
let wrapper;
+ let mutate;
const findUserAvatar = () => wrapper.findComponent(GlAvatar);
const findUserAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findUserLink = () => wrapper.findByTestId('user-link');
+ const findDesignNoteAwardsList = () => wrapper.findComponent(DesignNoteAwardsList);
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findEditButton = () => wrapper.findByTestId('note-edit');
const findNoteContent = () => wrapper.findByTestId('note-text');
@@ -43,97 +53,106 @@ describe('Design note component', () => {
const findEditDropdownItem = () => findDropdownItems().at(0);
const findDeleteDropdownItem = () => findDropdownItems().at(1);
- function createComponent(props = {}, data = { isEditing: false }) {
- wrapper = mountExtended(DesignNote, {
+ function createComponent({
+ props = {},
+ data = { isEditing: false },
+ mountFn = mountExtended,
+ mocks = {
+ $route,
+ $apollo: {
+ mutate: jest.fn().mockResolvedValue({ data: { updateNote: {} } }),
+ },
+ },
+ stubs = {
+ ApolloMutation,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ TimelineEntryItem: true,
+ TimeAgoTooltip: true,
+ GlAvatarLink: true,
+ GlAvatar: true,
+ GlLink: true,
+ },
+ } = {}) {
+ wrapper = mountFn(DesignNote, {
propsData: {
note: {},
noteableId: 'gid://gitlab/DesignManagement::Design/6',
+ designVariables: {
+ atVersion: null,
+ filenames: ['foo.jpg'],
+ fullPath: 'gitlab-org/gitlab-test',
+ iid: '1',
+ },
...props,
},
+ provide: {
+ issueIid: '1',
+ projectPath: 'gitlab-org/gitlab-test',
+ },
data() {
return {
...data,
};
},
- mocks: {
- $route,
- $apollo: {
- mutate,
- },
- },
- stubs: {
- ApolloMutation,
- GlDisclosureDropdown,
- GlDisclosureDropdownItem,
- TimelineEntryItem: true,
- TimeAgoTooltip: true,
- GlAvatarLink: true,
- GlAvatar: true,
- GlLink: true,
- },
+ mocks,
+ stubs,
});
}
- it('should match the snapshot', () => {
- createComponent({
- note,
- });
-
- expect(wrapper.element).toMatchSnapshot();
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
});
- it('should render avatar with correct props', () => {
- createComponent({
- note,
- });
-
- expect(findUserAvatar().props()).toMatchObject({
- src: note.author.avatarUrl,
- entityName: note.author.username,
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({ props: { note } });
});
- expect(findUserAvatarLink().attributes('href')).toBe(note.author.webUrl);
- });
+ it('should render avatar with correct props', () => {
+ expect(findUserAvatar().props()).toMatchObject({
+ src: note.author.avatarUrl,
+ entityName: note.author.username,
+ });
- it('should render author details', () => {
- createComponent({
- note,
+ expect(findUserAvatarLink().attributes()).toMatchObject({
+ href: note.author.webUrl,
+ 'data-user-id': '1',
+ 'data-username': `${note.author.username}`,
+ });
});
- expect(findUserLink().exists()).toBe(true);
- });
-
- it('should render a time ago tooltip if note has createdAt property', () => {
- createComponent({
- note,
+ it('should render author details', () => {
+ expect(findUserLink().exists()).toBe(true);
});
- expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
- });
-
- it('should not render edit icon when user does not have a permission', () => {
- createComponent({
- note,
+ it('should render a time ago tooltip if note has createdAt property', () => {
+ expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
});
- expect(findEditButton().exists()).toBe(false);
- });
+ it('should render emoji awards list', () => {
+ expect(findDesignNoteAwardsList().exists()).toBe(true);
+ });
- it('should not display a dropdown if user does not have a permission to delete note', () => {
- createComponent({
- note,
+ it('should not render edit icon when user does not have a permission', () => {
+ expect(findEditButton().exists()).toBe(false);
});
- expect(findDropdown().exists()).toBe(false);
+ it('should not display a dropdown if user does not have a permission to delete 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({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ },
},
},
});
@@ -147,25 +166,29 @@ describe('Design note component', () => {
describe('when edit form is rendered', () => {
beforeEach(() => {
- createComponent(
- {
+ createComponent({
+ props: {
note: {
...note,
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
},
},
- { isEditing: true },
- );
+ data: { isEditing: true },
+ });
});
it('should open an edit form on edit button click', async () => {
createComponent({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ },
},
},
});
@@ -203,10 +226,13 @@ describe('Design note component', () => {
describe('when user has admin permissions', () => {
it('should display a dropdown', () => {
createComponent({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ },
},
},
});
@@ -223,12 +249,15 @@ describe('Design note component', () => {
...note,
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
};
createComponent({
- note: {
- ...payload,
+ props: {
+ note: {
+ ...payload,
+ },
},
});
@@ -236,4 +265,91 @@ describe('Design note component', () => {
expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
});
+
+ describe('when user has award emoji permissions', () => {
+ const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
+ const propsData = {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ },
+ },
+ };
+
+ it('should render emoji-picker button', () => {
+ createComponent({ props: propsData, mountFn: shallowMountExtended });
+
+ const emojiPicker = findEmojiPicker();
+
+ expect(emojiPicker.exists()).toBe(true);
+ expect(emojiPicker.props()).toMatchObject({
+ boundary: 'viewport',
+ right: false,
+ });
+ });
+
+ it('should call mutation to add an emoji', () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiToggle: {
+ errors: [],
+ toggledOn: true,
+ },
+ },
+ });
+ createComponent({
+ props: propsData,
+ mountFn: shallowMountExtended,
+ mocks: {
+ $route,
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+
+ findEmojiPicker().vm.$emit('click', 'thumbsup');
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: designNoteAwardEmojiToggleMutation,
+ variables: {
+ name: 'thumbsup',
+ awardableId: note.id,
+ },
+ optimisticResponse: {
+ awardEmojiToggle: {
+ errors: [],
+ toggledOn: true,
+ },
+ },
+ update: expect.any(Function),
+ });
+ });
+
+ it('should emit an error when mutation fails', async () => {
+ jest.spyOn(Sentry, 'captureException');
+ mutate = jest.fn().mockRejectedValue({});
+ createComponent({
+ props: propsData,
+ mountFn: shallowMountExtended,
+ mocks: {
+ $route,
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+
+ findEmojiPicker().vm.$emit('click', 'thumbsup');
+
+ expect(mutate).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
+ expect(wrapper.emitted('error')).toEqual([[{}]]);
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index fdcea6d88c0..e64dec14461 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -220,10 +220,6 @@ describe('Design management design presentation component', () => {
);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('sets overlay position correctly when overlay is smaller than viewport', () => {
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
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 698535d8937..2262e5fdd83 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -50,10 +50,6 @@ describe('Design management design todo button', () => {
createComponent();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('renders TodoButton component', () => {
expect(wrapper.findComponent(TodoButton).exists()).toBe(true);
});
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 063df9366e9..0d004baafd0 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -1,3 +1,27 @@
+export const mockAuthor = {
+ id: 'gid://gitlab/User/1',
+ name: 'John',
+ webUrl: 'link-to-john-profile',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ username: 'john.doe',
+};
+
+export const mockAwardEmoji = {
+ __typename: 'AwardEmojiConnection',
+ nodes: [
+ {
+ __typename: 'AwardEmoji',
+ name: 'briefcase',
+ user: mockAuthor,
+ },
+ {
+ __typename: 'AwardEmoji',
+ name: 'baseball',
+ user: mockAuthor,
+ },
+ ],
+};
+
export const designListQueryResponseNodes = [
{
__typename: 'Design',
@@ -237,6 +261,9 @@ export const mockNoteSubmitSuccessMutationResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
body: 'New comment',
bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
createdAt: '2023-02-24T06:49:20Z',
@@ -257,6 +284,7 @@ export const mockNoteSubmitSuccessMutationResponse = {
userPermissions: {
adminNote: true,
repositionNote: true,
+ awardEmoji: true,
__typename: 'NotePermissions',
},
discussion: {
@@ -363,6 +391,7 @@ export const designFactory = ({
},
userPermissions: {
updateDesign,
+ awardEmoji: true,
__typename: 'IssuePermissions',
},
__typename: 'Issue',
diff --git a/spec/frontend/design_management/mock_data/discussion.js b/spec/frontend/design_management/mock_data/discussion.js
index 0e59ef29f8f..fbd5a9e0103 100644
--- a/spec/frontend/design_management/mock_data/discussion.js
+++ b/spec/frontend/design_management/mock_data/discussion.js
@@ -1,3 +1,5 @@
+import { mockAuthor, mockAwardEmoji } from './apollo_mock';
+
export default {
id: 'discussion-id-1',
resolved: false,
@@ -12,13 +14,12 @@ export default {
x: 10,
y: 15,
},
- author: {
- name: 'John',
- webUrl: 'link-to-john-profile',
- },
+ author: mockAuthor,
+ awardEmoji: mockAwardEmoji,
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
repositionNote: true,
+ awardEmoji: true,
},
resolved: false,
},
@@ -32,12 +33,15 @@ export default {
y: 25,
},
author: {
+ id: 'gid://gitlab/User/2',
name: 'Mary',
webUrl: 'link-to-mary-profile',
},
+ awardEmoji: mockAwardEmoji,
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
resolved: false,
},
diff --git a/spec/frontend/design_management/mock_data/notes.js b/spec/frontend/design_management/mock_data/notes.js
index 41cefaca05b..311ce4d1eb9 100644
--- a/spec/frontend/design_management/mock_data/notes.js
+++ b/spec/frontend/design_management/mock_data/notes.js
@@ -1,3 +1,4 @@
+import { mockAwardEmoji } from './apollo_mock';
import DISCUSSION_1 from './discussion';
const DISCUSSION_2 = {
@@ -17,9 +18,11 @@ const DISCUSSION_2 = {
name: 'Mary',
webUrl: 'link-to-mary-profile',
},
+ awardEmoji: mockAwardEmoji,
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
resolved: true,
},
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index b69452069c0..fb5cf4dfd0a 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -73,6 +73,8 @@ describe('diffs/components/app', () => {
propsData: {
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
+ endpointSast: '',
+ projectPath: 'namespace/project',
currentUser: {},
changesEmptyStateIllustration: '',
...props,
@@ -184,6 +186,16 @@ describe('diffs/components/app', () => {
});
});
+ describe('SAST diff', () => {
+ it('does not fetch Sast data on FOSS', () => {
+ createComponent();
+ jest.spyOn(wrapper.vm, 'fetchSast');
+ wrapper.vm.fetchData(false);
+
+ expect(wrapper.vm.fetchSast).not.toHaveBeenCalled();
+ });
+ });
+
it('displays loading icon on loading', () => {
createComponent({}, ({ state }) => {
state.diffs.isLoading = true;
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 3c092296130..fa16af92701 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -5,7 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
import Component from '~/diffs/components/commit_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
-import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status.vue';
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
diff --git a/spec/frontend/diffs/components/diff_code_quality_item_spec.js b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
index be9fb61a77d..085eb096239 100644
--- a/spec/frontend/diffs/components/diff_code_quality_item_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
@@ -2,20 +2,22 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
-import { multipleFindingsArr } from '../mock_data/diff_code_quality';
+import { multipleFindingsArrCodeQualityScale } from '../mock_data/diff_code_quality';
let wrapper;
+const [codeQualityFinding] = multipleFindingsArrCodeQualityScale;
const findIcon = () => wrapper.findComponent(GlIcon);
const findButton = () => wrapper.findComponent(GlLink);
const findDescriptionPlainText = () => wrapper.findByTestId('description-plain-text');
const findDescriptionLinkSection = () => wrapper.findByTestId('description-button-section');
describe('DiffCodeQuality', () => {
- const createWrapper = ({ glFeatures = {} } = {}) => {
+ const createWrapper = ({ glFeatures = {}, link = true } = {}) => {
return shallowMountExtended(DiffCodeQualityItem, {
propsData: {
- finding: multipleFindingsArr[0],
+ finding: codeQualityFinding,
+ link,
},
provide: {
glFeatures,
@@ -28,8 +30,8 @@ describe('DiffCodeQuality', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().attributes()).toMatchObject({
- class: `codequality-severity-icon ${SEVERITY_CLASSES[multipleFindingsArr[0].severity]}`,
- name: SEVERITY_ICONS[multipleFindingsArr[0].severity],
+ class: `codequality-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
+ name: SEVERITY_ICONS[codeQualityFinding.severity],
size: '12',
});
});
@@ -41,26 +43,35 @@ describe('DiffCodeQuality', () => {
codeQualityInlineDrawer: false,
},
});
- expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].severity);
- expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].description);
+ expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.severity);
+ expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.description);
});
});
describe('with codeQualityInlineDrawer flag true', () => {
- beforeEach(() => {
+ const [{ description, severity }] = multipleFindingsArrCodeQualityScale;
+ const renderedText = `${severity} - ${description}`;
+ it('when link prop is true, should render gl-link', () => {
wrapper = createWrapper({
glFeatures: {
codeQualityInlineDrawer: true,
},
});
- });
- it('should render severity as plain text', () => {
- expect(findDescriptionLinkSection().text()).toContain(multipleFindingsArr[0].severity);
+ expect(findButton().exists()).toBe(true);
+ expect(findButton().text()).toBe(renderedText);
});
- it('should render button with description text', () => {
- expect(findButton().text()).toContain(multipleFindingsArr[0].description);
+ it('when link prop is false, should not render gl-link', () => {
+ wrapper = createWrapper({
+ glFeatures: {
+ codeQualityInlineDrawer: true,
+ },
+ link: false,
+ });
+
+ expect(findButton().exists()).toBe(false);
+ expect(findDescriptionLinkSection().text()).toBe(renderedText);
});
});
});
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index 9ecfb62e1c5..73976ebd713 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -1,38 +1,61 @@
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
-import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
-import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
-import { multipleFindingsArr } from '../mock_data/diff_code_quality';
+import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
+import { NEW_CODE_QUALITY_FINDINGS, NEW_SAST_FINDINGS } from '~/diffs/i18n';
+import {
+ multipleCodeQualityNoSast,
+ multipleSastNoCodeQuality,
+} from '../mock_data/diff_code_quality';
let wrapper;
-const diffItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
-const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
+const diffInlineFindings = () => wrapper.findComponent(DiffInlineFindings);
+const allDiffInlineFindings = () => wrapper.findAllComponents(DiffInlineFindings);
describe('DiffCodeQuality', () => {
- const createWrapper = (codeQuality, mountFunction = mountExtended) => {
- return mountFunction(DiffCodeQuality, {
+ const createWrapper = (findings) => {
+ return mountExtended(DiffCodeQuality, {
propsData: {
expandedLines: [],
- codeQuality,
+ codeQuality: findings.codeQuality,
+ sast: findings.sast,
},
});
};
it('hides details and throws hideCodeQualityFindings event on close click', async () => {
- wrapper = createWrapper(multipleFindingsArr);
+ wrapper = createWrapper(multipleCodeQualityNoSast);
expect(wrapper.findByTestId('diff-codequality').exists()).toBe(true);
await wrapper.findByTestId('diff-codequality-close').trigger('click');
- expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1);
+ expect(wrapper.emitted('hideCodeQualityFindings')).toHaveLength(1);
});
- it('renders heading and correct amount of list items for codequality array and their description', () => {
- wrapper = createWrapper(multipleFindingsArr, shallowMountExtended);
+ it('renders diff inline findings component with correct props for codequality array', () => {
+ wrapper = createWrapper(multipleCodeQualityNoSast);
- expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
+ expect(diffInlineFindings().props('title')).toBe(NEW_CODE_QUALITY_FINDINGS);
+ expect(diffInlineFindings().props('findings')).toBe(multipleCodeQualityNoSast.codeQuality);
+ });
+
+ it('does not render codeQuality section when codeQuality array is empty', () => {
+ wrapper = createWrapper(multipleSastNoCodeQuality);
+
+ expect(diffInlineFindings().props('title')).toBe(NEW_SAST_FINDINGS);
+ expect(allDiffInlineFindings()).toHaveLength(1);
+ });
+
+ it('renders heading and correct amount of list items for sast array and their description', () => {
+ wrapper = createWrapper(multipleSastNoCodeQuality);
+
+ expect(diffInlineFindings().props('title')).toBe(NEW_SAST_FINDINGS);
+ expect(diffInlineFindings().props('findings')).toBe(multipleSastNoCodeQuality.sast);
+ });
+
+ it('does not render sast section when sast array is empty', () => {
+ wrapper = createWrapper(multipleCodeQualityNoSast);
- expect(diffItems()).toHaveLength(multipleFindingsArr.length);
- expect(diffItems().at(0).props().finding).toEqual(multipleFindingsArr[0]);
+ expect(diffInlineFindings().props('title')).toBe(NEW_CODE_QUALITY_FINDINGS);
+ expect(allDiffInlineFindings()).toHaveLength(1);
});
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 39d9255aaf9..3b37edbcb1d 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -2,6 +2,10 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
+import * as diffRowUtils from 'ee_else_ce/diffs/components/diff_row_utils';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import DiffView from '~/diffs/components/diff_view.vue';
@@ -10,9 +14,11 @@ import { diffViewerModes } from '~/ide/constants';
import NoteForm from '~/notes/components/note_form.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
+import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
import { getDiffFileMock } from '../mock_data/diff_file';
Vue.use(Vuex);
+jest.mock('~/alert');
describe('DiffContent', () => {
let wrapper;
@@ -72,6 +78,7 @@ describe('DiffContent', () => {
getCommentFormForDiffFile: getCommentFormForDiffFileGetterMock,
diffLines: () => () => [...getDiffFileMock().parallel_diff_lines],
fileLineCodequality: () => () => [],
+ fileLineSast: () => () => [],
},
actions: {
saveDiffDiscussion: saveDiffDiscussionMock,
@@ -113,6 +120,32 @@ describe('DiffContent', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+
+ it('should include Sast findings when sastReportsInInlineDiff flag is true', () => {
+ const mapParallelSpy = jest.spyOn(diffRowUtils, 'mapParallel');
+ const mapParallelNoSastSpy = jest.spyOn(diffRowUtils, 'mapParallelNoSast');
+ createComponent({
+ provide: {
+ glFeatures: {
+ sastReportsInInlineDiff: true,
+ },
+ },
+ props: { diffFile: { ...textDiffFile, renderingLines: true } },
+ });
+
+ expect(mapParallelSpy).toHaveBeenCalled();
+ expect(mapParallelNoSastSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not include Sast findings when sastReportsInInlineDiff flag is false', () => {
+ const mapParallelSpy = jest.spyOn(diffRowUtils, 'mapParallel');
+ const mapParallelNoSastSpy = jest.spyOn(diffRowUtils, 'mapParallelNoSast');
+
+ createComponent({ props: { diffFile: { ...textDiffFile, renderingLines: true } } });
+
+ expect(mapParallelNoSastSpy).toHaveBeenCalled();
+ expect(mapParallelSpy).not.toHaveBeenCalled();
+ });
});
describe('with whitespace only change', () => {
@@ -218,5 +251,44 @@ describe('DiffContent', () => {
},
});
});
+
+ describe('when note-form emits `handleFormUpdate`', () => {
+ const noteStub = {};
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ describe.each`
+ scenario | serverError | message
+ ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED}
+ ${'without server error'} | ${null} | ${SOMETHING_WENT_WRONG}
+ `('$scenario', ({ serverError, message }) => {
+ beforeEach(async () => {
+ saveDiffDiscussionMock.mockRejectedValue({ response: serverError });
+
+ createComponent({
+ props: {
+ diffFile: imageDiffFile,
+ },
+ });
+
+ wrapper
+ .findComponent(NoteForm)
+ .vm.$emit('handleFormUpdate', noteStub, parentElement, errorCallback);
+
+ await waitForPromises();
+ });
+
+ it(`renders ${serverError ? 'server' : 'generic'} error message`, () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(message, { reason: serverError?.data?.errors }),
+ parent: parentElement,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 73d9f2d6d45..40c617da0aa 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -25,11 +25,13 @@ describe('DiffDiscussions', () => {
});
};
+ const findNoteableDiscussion = () => wrapper.findComponent(NoteableDiscussion);
+
describe('template', () => {
it('should have notes list', () => {
createComponent();
- expect(wrapper.findComponent(NoteableDiscussion).exists()).toBe(true);
+ expect(findNoteableDiscussion().exists()).toBe(true);
expect(wrapper.findComponent(DiscussionNotes).exists()).toBe(true);
expect(
wrapper.findComponent(DiscussionNotes).findAllComponents(TimelineEntryItem).length,
@@ -51,11 +53,11 @@ describe('DiffDiscussions', () => {
it('dispatches toggleDiscussion when clicking collapse button', () => {
createComponent({ shouldCollapseDiscussions: true });
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
- const diffNotesToggle = findDiffNotesToggle();
- diffNotesToggle.trigger('click');
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ findDiffNotesToggle().trigger('click');
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ expect(store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
discussionId: discussionsMockData.id,
});
});
@@ -77,12 +79,12 @@ describe('DiffDiscussions', () => {
discussions[0].expanded = false;
createComponent({ discussions, shouldCollapseDiscussions: true });
- expect(wrapper.findComponent(NoteableDiscussion).isVisible()).toBe(false);
+ expect(findNoteableDiscussion().isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
createComponent({ renderAvatarBadge: true });
- const noteableDiscussion = wrapper.findComponent(NoteableDiscussion);
+ const noteableDiscussion = findNoteableDiscussion();
expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true);
expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1');
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index 3f75b086368..d3afaab492d 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -644,22 +644,14 @@ describe('DiffFileHeader component', () => {
);
});
- it.each`
- commentOnFiles | exists | existsText
- ${false} | ${false} | ${'does not'}
- ${true} | ${true} | ${'does'}
- `(
- '$existsText render comment on files button when commentOnFiles is $commentOnFiles',
- ({ commentOnFiles, exists }) => {
- window.gon = { current_user_id: 1 };
- createComponent({
- props: {
- addMergeRequestButtons: true,
- },
- options: { provide: { glFeatures: { commentOnFiles } } },
- });
+ it('should render the comment on files button', () => {
+ window.gon = { current_user_id: 1 };
+ createComponent({
+ props: {
+ addMergeRequestButtons: true,
+ },
+ });
- expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(exists);
- },
- );
+ expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(true);
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index d9c57ed1470..db6cde883f3 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -1,7 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import waitForPromises from 'helpers/wait_for_promises';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
import DiffContentComponent from 'jh_else_ce/diffs/components/diff_content.vue';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
@@ -11,19 +15,33 @@ import {
EVT_EXPAND_ALL_FILES,
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
+ FILE_DIFF_POSITION_TYPE,
} from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
-import createDiffsStore from '~/diffs/store/modules';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
import { scrollToElement } from '~/lib/utils/common_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createNotesStore from '~/notes/stores/modules';
+import diffsModule from '~/diffs/store/modules';
+import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
+import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { getDiffFileMock } from '../mock_data/diff_file';
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
+import diffsMockData from '../mock_data/merge_request_diffs';
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/alert');
+jest.mock('~/notes/mixins/diff_line_note_form', () => ({
+ methods: {
+ addToReview: jest.fn(),
+ },
+}));
+
+Vue.use(Vuex);
+
+const saveDiffDiscussionMock = jest.fn();
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
const file = store.state.diffs.diffFiles[index];
@@ -70,18 +88,29 @@ function markFileToBeRendered(store, index = 0) {
}
function createComponent({ file, first = false, last = false, options = {}, props = {} }) {
- Vue.use(Vuex);
+ const diffs = diffsModule();
+ diffs.actions = {
+ ...diffs.actions,
+ saveDiffDiscussion: saveDiffDiscussionMock,
+ };
+
+ diffs.getters = {
+ ...diffs.getters,
+ diffCompareDropdownTargetVersions: () => [],
+ diffCompareDropdownSourceVersions: () => [],
+ };
const store = new Vuex.Store({
...createNotesStore(),
- modules: {
- diffs: createDiffsStore(),
- },
+ modules: { diffs },
});
- store.state.diffs.diffFiles = [file];
+ store.state.diffs = {
+ mergeRequestDiff: diffsMockData[0],
+ diffFiles: [file],
+ };
- const wrapper = shallowMount(DiffFileComponent, {
+ const wrapper = shallowMountExtended(DiffFileComponent, {
store,
propsData: {
file,
@@ -101,9 +130,10 @@ function createComponent({ file, first = false, last = false, options = {}, prop
}
const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent);
-const findDiffContentArea = (wrapper) => wrapper.find('[data-testid="content-area"]');
-const findLoader = (wrapper) => wrapper.find('[data-testid="loader-icon"]');
-const findToggleButton = (wrapper) => wrapper.find('[data-testid="expand-button"]');
+const findDiffContentArea = (wrapper) => wrapper.findByTestId('content-area');
+const findLoader = (wrapper) => wrapper.findByTestId('loader-icon');
+const findToggleButton = (wrapper) => wrapper.findByTestId('expand-button');
+const findNoteForm = (wrapper) => wrapper.findByTestId('file-note-form');
const toggleFile = (wrapper) => findDiffHeader(wrapper).vm.$emit('toggleFile');
const getReadableFile = () => getDiffFileMock();
@@ -118,6 +148,12 @@ const makeFileManuallyCollapsed = (store, index = 0) =>
const changeViewerType = (store, newType, index = 0) =>
changeViewer(store, index, { name: diffViewerModes[newType] });
+const triggerSaveNote = (wrapper, note, parent, error) =>
+ findNoteForm(wrapper).vm.$emit('handleFormUpdate', note, parent, error);
+
+const triggerSaveDraftNote = (wrapper, note, parent, error) =>
+ findNoteForm(wrapper).vm.$emit('handleFormUpdateAddToReview', note, false, parent, error);
+
describe('DiffFile', () => {
let wrapper;
let store;
@@ -502,7 +538,7 @@ describe('DiffFile', () => {
await nextTick();
- const button = wrapper.find('[data-testid="blob-button"]');
+ const button = wrapper.findByTestId('blob-button');
expect(wrapper.text()).toContain('Changes are too large to be shown.');
expect(button.html()).toContain('View file @');
@@ -510,24 +546,6 @@ describe('DiffFile', () => {
});
});
- it('loads collapsed file on mounted when single file mode is enabled', async () => {
- const file = {
- ...getReadableFile(),
- load_collapsed_diff_url: '/diff_for_path',
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
- viewer: { name: 'collapsed', automaticallyCollapsed: true },
- };
-
- axiosMock.onGet(file.load_collapsed_diff_url).reply(HTTP_STATUS_OK, getReadableFile());
-
- ({ wrapper, store } = createComponent({ file, props: { viewDiffsFileByFile: true } }));
-
- await nextTick();
-
- expect(findLoader(wrapper).exists()).toBe(true);
- });
-
describe('merge conflicts', () => {
it('does not render conflict alert', () => {
const file = {
@@ -538,7 +556,7 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({ file }));
- expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(false);
+ expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(false);
});
it('renders conflict alert when conflict_type is present', () => {
@@ -550,7 +568,7 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({ file }));
- expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(true);
+ expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(true);
});
});
@@ -572,10 +590,9 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
- options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
- expect(wrapper.find('[data-testid="file-discussions"]').exists()).toEqual(exists);
+ expect(wrapper.findByTestId('file-discussions').exists()).toEqual(exists);
},
);
@@ -593,10 +610,9 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
- options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
- expect(wrapper.find('[data-testid="file-note-form"]').exists()).toEqual(exists);
+ expect(findNoteForm(wrapper).exists()).toEqual(exists);
},
);
@@ -612,10 +628,99 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
- options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
- expect(wrapper.find('[data-testid="diff-file-discussions"]').exists()).toEqual(exists);
+ expect(wrapper.findByTestId('diff-file-discussions').exists()).toEqual(exists);
+ });
+
+ describe('when note-form emits `handleFormUpdate`', () => {
+ const file = {
+ ...getReadableFile(),
+ hasCommentForm: true,
+ };
+
+ const note = {};
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ beforeEach(() => {
+ ({ wrapper, store } = createComponent({
+ file,
+ options: { provide: { glFeatures: { commentOnFiles: true } } },
+ }));
+ });
+
+ it('calls saveDiffDiscussionMock', () => {
+ triggerSaveNote(wrapper, note, parentElement, errorCallback);
+
+ expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), {
+ note,
+ formData: {
+ noteableData: expect.any(Object),
+ diffFile: file,
+ positionType: FILE_DIFF_POSITION_TYPE,
+ noteableType: store.getters.noteableType,
+ },
+ });
+ });
+
+ describe('when saveDiffDiscussionMock throws an error', () => {
+ describe.each`
+ scenario | serverError | message
+ ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED}
+ ${'without server error'} | ${{}} | ${SOMETHING_WENT_WRONG}
+ `('$scenario', ({ serverError, message }) => {
+ beforeEach(async () => {
+ saveDiffDiscussionMock.mockRejectedValue({ response: serverError });
+
+ triggerSaveNote(wrapper, note, parentElement, errorCallback);
+
+ await waitForPromises();
+ });
+
+ it(`renders ${serverError ? 'server' : 'generic'} error message`, () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(message, { reason: serverError?.data?.errors }),
+ parent: parentElement,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('when note-form emits `handleFormUpdateAddToReview`', () => {
+ const file = {
+ ...getReadableFile(),
+ hasCommentForm: true,
+ };
+
+ const note = {};
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ beforeEach(async () => {
+ ({ wrapper, store } = createComponent({
+ file,
+ options: { provide: { glFeatures: { commentOnFiles: true } } },
+ }));
+
+ triggerSaveDraftNote(wrapper, note, parentElement, errorCallback);
+
+ await nextTick();
+ });
+
+ it('calls addToReview mixin', () => {
+ expect(diffLineNoteFormMixin.methods.addToReview).toHaveBeenCalledWith(
+ note,
+ FILE_DIFF_POSITION_TYPE,
+ parentElement,
+ errorCallback,
+ );
+ });
});
});
});
diff --git a/spec/frontend/diffs/components/diff_inline_findings_spec.js b/spec/frontend/diffs/components/diff_inline_findings_spec.js
new file mode 100644
index 00000000000..9ccfb2a613d
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_inline_findings_spec.js
@@ -0,0 +1,33 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
+import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
+import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
+import { multipleCodeQualityNoSast } from '../mock_data/diff_code_quality';
+
+let wrapper;
+const heading = () => wrapper.findByTestId('diff-inline-findings-heading');
+const diffCodeQualityItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
+
+describe('DiffInlineFindings', () => {
+ const createWrapper = () => {
+ return shallowMountExtended(DiffInlineFindings, {
+ propsData: {
+ title: NEW_CODE_QUALITY_FINDINGS,
+ findings: multipleCodeQualityNoSast.codeQuality,
+ },
+ });
+ };
+
+ it('renders the title correctly', () => {
+ wrapper = createWrapper();
+ expect(heading().text()).toBe(NEW_CODE_QUALITY_FINDINGS);
+ });
+
+ it('renders the correct number of DiffCodeQualityItem components with correct props', () => {
+ wrapper = createWrapper();
+ expect(diffCodeQualityItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
+ expect(diffCodeQualityItems().wrappers[0].props('finding')).toEqual(
+ wrapper.props('findings')[0],
+ );
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index e42b98e4d68..0ca48db2497 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,15 +1,20 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import store from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { noteableDataMock } from 'jest/notes/mock_data';
+import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
import { getDiffFileMock } from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
+jest.mock('~/alert');
describe('DiffLineNoteForm', () => {
let wrapper;
@@ -17,6 +22,8 @@ describe('DiffLineNoteForm', () => {
let diffLines;
beforeEach(() => {
+ store.reset();
+
diffFile = getDiffFileMock();
diffLines = diffFile.highlighted_diff_lines;
@@ -214,5 +221,38 @@ describe('DiffLineNoteForm', () => {
fileHash: diffFile.file_hash,
});
});
+
+ describe('when note-form emits `handleFormUpdate`', () => {
+ const noteStub = 'invalid note';
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ describe.each`
+ scenario | serverError | message
+ ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED}
+ ${'without server error'} | ${null} | ${SOMETHING_WENT_WRONG}
+ `('$scenario', ({ serverError, message }) => {
+ beforeEach(async () => {
+ store.dispatch.mockRejectedValue({ response: serverError });
+
+ createComponent();
+
+ await findNoteForm().vm.$emit('handleFormUpdate', noteStub, parentElement, errorCallback);
+
+ await waitForPromises();
+ });
+
+ it(`renders ${serverError ? 'server' : 'generic'} error message`, () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(message, { reason: serverError?.data?.errors }),
+ parent: parentElement,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_line_spec.js b/spec/frontend/diffs/components/diff_line_spec.js
index 37368eb1461..a552a9d3e7f 100644
--- a/spec/frontend/diffs/components/diff_line_spec.js
+++ b/spec/frontend/diffs/components/diff_line_spec.js
@@ -16,6 +16,13 @@ const left = {
severity: EXAMPLE_SEVERITY,
},
],
+ sast: [
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ],
},
},
};
@@ -30,6 +37,13 @@ const right = {
severity: EXAMPLE_SEVERITY,
},
],
+ sast: [
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ],
},
},
};
@@ -60,6 +74,13 @@ describe('DiffLine', () => {
severity: EXAMPLE_SEVERITY,
},
]);
+ expect(wrapper.findComponent(DiffCodeQuality).props('sast')).toEqual([
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ]);
});
});
});
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index 356c7ef925a..119b8f9ad7f 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -33,6 +33,14 @@ describe('DiffRow', () => {
left: { old_line: 1, discussions: [] },
right: { new_line: 1, discussions: [] },
},
+ {
+ left: {},
+ right: {},
+ isMetaLineLeft: true,
+ isMetaLineRight: false,
+ isContextLineLeft: true,
+ isContextLineRight: false,
+ },
];
const createWrapper = ({ props, state = {}, actions, isLoggedIn = true }) => {
@@ -273,6 +281,12 @@ describe('DiffRow', () => {
expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
});
});
+
+ it('renders comment button when isMetaLineLeft is false and isMetaLineRight is true', () => {
+ wrapper = createWrapper({ props: { line: testLines[4], inline: false } });
+
+ expect(wrapper.find('.add-diff-note').exists()).toBe(true);
+ });
});
describe('coverage state memoization', () => {
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 1ec8547d325..f56dd28ce9c 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,4 +1,3 @@
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
@@ -6,18 +5,21 @@ import createStore from '~/diffs/store/modules';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import DiffFileRow from '~/diffs/components//diff_file_row.vue';
import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Diffs tree list component', () => {
let wrapper;
let store;
const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' });
const getFileRow = () => wrapper.findComponent(DiffFileRow);
+ const findDiffTreeSearch = () => wrapper.findByTestId('diff-tree-search');
+
Vue.use(Vuex);
- const createComponent = () => {
- wrapper = shallowMount(TreeList, {
+ const createComponent = ({ hideFileStats = false } = {}) => {
+ wrapper = shallowMountExtended(TreeList, {
store,
- propsData: { hideFileStats: false },
+ propsData: { hideFileStats },
stubs: {
// eslint will fail if we import the real component
RecycleScroller: stubComponent(
@@ -116,7 +118,10 @@ 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');
+ const input = findDiffTreeSearch();
+
+ input.element.value = '*.md';
+ input.trigger('input');
await nextTick();
@@ -131,7 +136,10 @@ describe('Diffs tree list component', () => {
${'app/*.js'} | ${2}
${'*.js, *.rb'} | ${3}
`('returns $itemSize item for $extension', async ({ extension, itemSize }) => {
- wrapper.find('[data-testid="diff-tree-search"]').setValue(extension);
+ const input = findDiffTreeSearch();
+
+ input.element.value = extension;
+ input.trigger('input');
await nextTick();
@@ -143,23 +151,21 @@ describe('Diffs tree list component', () => {
expect(getScroller().props('items')).toHaveLength(2);
});
- it('hides file stats', async () => {
- wrapper.setProps({ hideFileStats: true });
-
- await nextTick();
- expect(wrapper.find('.file-row-stats').exists()).toBe(false);
+ it('hides file stats', () => {
+ createComponent({ hideFileStats: true });
+ expect(getFileRow().props('hideFileStats')).toBe(true);
});
it('calls toggleTreeOpen when clicking folder', () => {
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
+ jest.spyOn(store, 'dispatch').mockReturnValue(undefined);
getFileRow().vm.$emit('toggleTreeOpen', 'app');
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
});
it('renders when renderTreeList is false', async () => {
- wrapper.vm.$store.state.diffs.renderTreeList = false;
+ store.state.diffs.renderTreeList = false;
await nextTick();
expect(getScroller().props('items')).toHaveLength(3);
@@ -178,7 +184,7 @@ describe('Diffs tree list component', () => {
createComponent();
await nextTick();
- expect(wrapper.findComponent(DiffFileRow).props('viewedFiles')).toBe(viewedDiffFileIds);
+ expect(getFileRow().props('viewedFiles')).toBe(viewedDiffFileIds);
});
});
});
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index 29f16da8d89..5b9ed538e01 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -1,49 +1,120 @@
-export const multipleFindingsArr = [
+export const multipleFindingsArrCodeQualityScale = [
{
severity: 'minor',
description: 'mocked minor Issue',
line: 2,
+ scale: 'codeQuality',
},
{
severity: 'major',
description: 'mocked major Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'info',
description: 'mocked info Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'critical',
description: 'mocked critical Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'blocker',
description: 'mocked blocker Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'unknown',
description: 'mocked unknown Issue',
line: 3,
+ scale: 'codeQuality',
},
];
-export const fiveFindings = {
+export const multipleFindingsArrSastScale = [
+ {
+ severity: 'low',
+ description: 'mocked low Issue',
+ line: 2,
+ scale: 'sast',
+ },
+ {
+ severity: 'medium',
+ description: 'mocked medium Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'info',
+ description: 'mocked info Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'high',
+ description: 'mocked high Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'critical',
+ description: 'mocked critical Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'unknown',
+ description: 'mocked unknown Issue',
+ line: 3,
+ scale: 'sast',
+ },
+];
+
+export const multipleCodeQualityNoSast = {
+ codeQuality: multipleFindingsArrCodeQualityScale,
+ sast: [],
+};
+
+export const multipleSastNoCodeQuality = {
+ codeQuality: [],
+ sast: multipleFindingsArrSastScale,
+};
+
+export const fiveCodeQualityFindings = {
+ filePath: 'index.js',
+ codequality: multipleFindingsArrCodeQualityScale.slice(0, 5),
+};
+
+export const threeCodeQualityFindings = {
+ filePath: 'index.js',
+ codequality: multipleFindingsArrCodeQualityScale.slice(0, 3),
+};
+
+export const singularCodeQualityFinding = {
+ filePath: 'index.js',
+ codequality: [multipleFindingsArrCodeQualityScale[0]],
+};
+
+export const singularFindingSast = {
filePath: 'index.js',
- codequality: multipleFindingsArr.slice(0, 5),
+ sast: [multipleFindingsArrSastScale[0]],
};
-export const threeFindings = {
+export const threeSastFindings = {
filePath: 'index.js',
- codequality: multipleFindingsArr.slice(0, 3),
+ sast: multipleFindingsArrSastScale.slice(0, 3),
};
-export const singularFinding = {
+export const oneCodeQualityTwoSastFindings = {
filePath: 'index.js',
- codequality: [multipleFindingsArr[0]],
+ sast: multipleFindingsArrSastScale.slice(0, 2),
+ codequality: [multipleFindingsArrCodeQualityScale[0]],
};
export const diffCodeQuality = {
@@ -73,7 +144,7 @@ export const diffCodeQuality = {
old_line: null,
new_line: 2,
- codequality: [multipleFindingsArr[0]],
+ codequality: [multipleFindingsArrCodeQualityScale[0]],
lineDrafts: [],
},
},
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 7534fe741e7..bbe748b8e1f 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -11,7 +11,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
EVT_MR_PREPARED,
} from '~/diffs/constants';
-import { LOAD_SINGLE_DIFF_FAILED } from '~/diffs/i18n';
+import { LOAD_SINGLE_DIFF_FAILED, BUILDING_YOUR_MR, SOMETHING_WENT_WRONG } from '~/diffs/i18n';
import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
@@ -87,6 +87,7 @@ describe('DiffsStoreActions', () => {
a: ['z', 'hash:a'],
b: ['y', 'hash:a'],
};
+ const diffViewType = 'inline';
return testAction(
diffActions.setBaseConfig,
@@ -100,6 +101,7 @@ describe('DiffsStoreActions', () => {
dismissEndpoint,
showSuggestPopover,
mrReviews,
+ diffViewType,
},
{
endpoint: '',
@@ -124,6 +126,7 @@ describe('DiffsStoreActions', () => {
dismissEndpoint,
showSuggestPopover,
mrReviews,
+ diffViewType,
},
},
{
@@ -362,7 +365,7 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
- [{ type: 'startRenderDiffsQueue' }, { type: 'startRenderDiffsQueue' }],
+ [],
);
});
});
@@ -418,9 +421,7 @@ describe('DiffsStoreActions', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching(
- 'Building your merge request… This page will update when the build is complete.',
- ),
+ message: BUILDING_YOUR_MR,
variant: 'warning',
});
});
@@ -482,7 +483,7 @@ describe('DiffsStoreActions', () => {
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching('Something went wrong'),
+ message: SOMETHING_WENT_WRONG,
});
});
});
@@ -663,41 +664,6 @@ describe('DiffsStoreActions', () => {
});
});
- describe('startRenderDiffsQueue', () => {
- it('should set all files to RENDER_FILE', () => {
- const state = {
- diffFiles: [
- {
- id: 1,
- renderIt: false,
- viewer: {
- automaticallyCollapsed: false,
- },
- },
- {
- id: 2,
- renderIt: false,
- viewer: {
- automaticallyCollapsed: false,
- },
- },
- ],
- };
-
- const pseudoCommit = (commitType, file) => {
- expect(commitType).toBe(types.RENDER_FILE);
- Object.assign(file, {
- renderIt: true,
- });
- };
-
- diffActions.startRenderDiffsQueue({ state, commit: pseudoCommit });
-
- expect(state.diffFiles[0].renderIt).toBe(true);
- expect(state.diffFiles[1].renderIt).toBe(true);
- });
- });
-
describe('setInlineDiffViewType', () => {
it('should set diff view type to inline and also set the cookie properly', async () => {
await testAction(
@@ -1285,12 +1251,11 @@ describe('DiffsStoreActions', () => {
$emit = jest.spyOn(eventHub, '$emit');
});
- it('renders and expands file for the given discussion id', () => {
+ it('expands the file for the given discussion id', () => {
const localState = state({ collapsed: true, renderIt: false });
diffActions.renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
- expect(commit).toHaveBeenCalledWith('RENDER_FILE', localState.diffFiles[0]);
expect($emit).toHaveBeenCalledTimes(1);
expect(commonUtils.scrollToElement).toHaveBeenCalledTimes(1);
});
@@ -1377,18 +1342,6 @@ describe('DiffsStoreActions', () => {
});
});
- describe('setRenderIt', () => {
- it('commits RENDER_FILE', () => {
- return testAction(
- diffActions.setRenderIt,
- 'file',
- {},
- [{ type: types.RENDER_FILE, payload: 'file' }],
- [],
- );
- });
- });
-
describe('receiveFullDiffError', () => {
it('updates state with the file that did not load', () => {
return testAction(
@@ -1513,7 +1466,7 @@ describe('DiffsStoreActions', () => {
payload: { filePath: testFilePath, lines: [preparedLine, preparedLine] },
},
],
- [{ type: 'startRenderDiffsQueue' }],
+ [],
);
},
);
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index b089cf22b14..274cb40dac8 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -12,6 +12,7 @@ describe('DiffsStoreMutations', () => {
${'endpoint'} | ${'/diffs/endpoint'}
${'projectPath'} | ${'/root/project'}
${'endpointUpdateUser'} | ${'/user/preferences'}
+ ${'diffViewType'} | ${'parallel'}
`('should set the $prop property into state', ({ prop, value }) => {
const state = {};
@@ -104,7 +105,6 @@ describe('DiffsStoreMutations', () => {
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 888df06d6b9..117ed56e347 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -437,7 +437,7 @@ describe('DiffsStoreUtils', () => {
});
});
- it('sets the renderIt and collapsed attribute on files', () => {
+ it('sets the collapsed attribute on files', () => {
const checkLine = preparedDiff.diff_files[0][INLINE_DIFF_LINES_KEY][0];
expect(checkLine.discussions.length).toBe(0);
@@ -448,7 +448,6 @@ describe('DiffsStoreUtils', () => {
expect(firstChar).not.toBe('+');
expect(firstChar).not.toBe('-');
- expect(preparedDiff.diff_files[0].renderIt).toBe(true);
expect(preparedDiff.diff_files[0].collapsed).toBe(false);
});
@@ -529,8 +528,7 @@ describe('DiffsStoreUtils', () => {
preparedDiffFiles = utils.prepareDiffData({ diff: mock, meta: true });
});
- it('sets the renderIt and collapsed attribute on files', () => {
- expect(preparedDiffFiles[0].renderIt).toBe(true);
+ it('sets the collapsed attribute on files', () => {
expect(preparedDiffFiles[0].collapsed).toBeUndefined();
});
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
index 4d93908b757..5a77b9d4689 100644
--- a/spec/frontend/drawio/drawio_editor_spec.js
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -66,7 +66,6 @@ describe('drawio/drawio_editor', () => {
});
afterEach(() => {
- jest.clearAllMocks();
findDrawioIframe()?.remove();
});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 57debf79c7b..ba4d838e44b 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html';
import mock from 'xhr-mock';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,6 +8,7 @@ import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import htmlNewMilestone from 'test_fixtures_static/textarea.html';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 70bc1dee0ee..c820d6ac63d 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -56,7 +56,6 @@ describe('The basis for an Source Editor extension', () => {
});
afterEach(() => {
- jest.clearAllMocks();
resetHTMLFixture();
});
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 512b298bbbd..d9e1a22d60d 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -182,10 +182,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
instance.togglePreview();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('does not do anything if there is no model', () => {
instance.setModel(null);
@@ -199,9 +195,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
await togglePreview();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
it('removes the registered buttons from the toolbar', () => {
expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
index 14ec7f8b93f..4b1ed0fbb42 100644
--- a/spec/frontend/editor/source_editor_yaml_ext_spec.js
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -368,10 +368,6 @@ abc: def
let highlightLinesSpy;
let removeHighlightsSpy;
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it.each`
highlightPathOnSetup | path | keepOnNotFound | expectHighlightLinesToBeCalled | withLines | expectRemoveHighlightsToBeCalled | storedHighlightPath
${null} | ${undefined} | ${false} | ${false} | ${undefined} | ${true} | ${null}
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 36c3eeb5a52..1b948cce73a 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -7,6 +7,7 @@ import {
clearEmojiMock,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
+import { createMockClient } from 'helpers/mock_apollo_helper';
import {
glEmojiTag,
searchEmoji,
@@ -14,6 +15,8 @@ import {
sortEmoji,
initEmojiMap,
getAllEmoji,
+ emojiFallbackImageSrc,
+ loadCustomEmojiWithNames,
} from '~/emoji';
import isEmojiUnicodeSupported, {
@@ -25,6 +28,12 @@ import isEmojiUnicodeSupported, {
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
+import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
+
+let mockClient;
+jest.mock('~/lib/graphql', () => {
+ return () => mockClient;
+});
const emptySupportMap = {
personZwj: false,
@@ -45,12 +54,35 @@ const emptySupportMap = {
1.1: false,
};
+function createMockEmojiClient() {
+ mockClient = createMockClient([
+ [
+ customEmojiQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ group: {
+ id: 1,
+ customEmoji: {
+ nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+
+ window.gon = { features: { customEmoji: true } };
+ document.body.dataset.groupFullPath = 'test-group';
+}
+
describe('emoji', () => {
beforeEach(async () => {
await initEmojiMock();
});
afterEach(() => {
+ window.gon = {};
+ delete document.body.dataset.groupFullPath;
clearEmojiMock();
});
@@ -690,4 +722,67 @@ describe('emoji', () => {
expect(scoredItems.sort(sortEmoji)).toEqual(expected);
});
});
+
+ describe('emojiFallbackImageSrc', () => {
+ beforeEach(async () => {
+ createMockEmojiClient();
+
+ await initEmojiMock();
+ });
+
+ it.each`
+ emoji | src
+ ${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'}
+ ${'parrot'} | ${'parrot.gif'}
+ `('returns $src for emoji with name $emoji', ({ emoji, src }) => {
+ expect(emojiFallbackImageSrc(emoji)).toBe(src);
+ });
+ });
+
+ describe('loadCustomEmojiWithNames', () => {
+ beforeEach(() => {
+ createMockEmojiClient();
+ });
+
+ describe('flag disabled', () => {
+ beforeEach(() => {
+ window.gon = {};
+ });
+
+ it('returns empty object', async () => {
+ const result = await loadCustomEmojiWithNames();
+
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('when not in a group', () => {
+ beforeEach(() => {
+ delete document.body.dataset.groupFullPath;
+ });
+
+ it('returns empty object', async () => {
+ const result = await loadCustomEmojiWithNames();
+
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('when in a group with flag enabled', () => {
+ it('returns empty object', async () => {
+ const result = await loadCustomEmojiWithNames();
+
+ expect(result).toEqual({
+ parrot: {
+ c: 'custom',
+ d: 'parrot',
+ e: undefined,
+ name: 'parrot',
+ src: 'parrot.gif',
+ u: 'custom',
+ },
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index f436c96f4a5..93fe9ed9400 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -1,15 +1,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
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 '~/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';
import getEnvironment from '~/environments/graphql/queries/environment.query.graphql';
+import getEnvironmentWithNamespace from '~/environments/graphql/queries/environment_with_namespace.graphql';
import updateEnvironment from '~/environments/graphql/mutations/update_environment.mutation.graphql';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
@@ -17,15 +15,15 @@ import createMockApollo from '../__helpers__/mock_apollo_helper';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
-const newExternalUrl = 'https://google.ca';
const environment = {
id: '1',
name: 'foo',
externalUrl: 'https://foo.example.com',
clusterAgent: null,
+ kubernetesNamespace: null,
};
const resolvedEnvironment = { project: { id: '1', environment } };
-const environmentUpdate = {
+const environmentUpdateSuccess = {
environment: { id: '1', path: 'path/to/environment', clusterAgentId: null },
errors: [],
};
@@ -36,46 +34,51 @@ const environmentUpdateError = {
const provide = {
projectEnvironmentsPath: '/projects/environments',
- updateEnvironmentPath: '/projects/environments/1',
protectedEnvironmentSettingsPath: '/projects/1/settings/ci_cd',
projectPath: '/path/to/project',
+ environmentName: 'foo',
};
describe('~/environments/components/edit.vue', () => {
let wrapper;
- let mock;
- const createMockApolloProvider = (mutationResult) => {
+ const getEnvironmentQuery = jest.fn().mockResolvedValue({ data: resolvedEnvironment });
+ const getEnvironmentWithNamespaceQuery = jest
+ .fn()
+ .mockResolvedValue({ data: resolvedEnvironment });
+
+ const updateEnvironmentSuccess = jest
+ .fn()
+ .mockResolvedValue({ data: { environmentUpdate: environmentUpdateSuccess } });
+ const updateEnvironmentFail = jest
+ .fn()
+ .mockResolvedValue({ data: { environmentUpdate: environmentUpdateError } });
+
+ const createMockApolloProvider = (mutationHandler) => {
Vue.use(VueApollo);
const mocks = [
- [getEnvironment, jest.fn().mockResolvedValue({ data: resolvedEnvironment })],
- [
- updateEnvironment,
- jest.fn().mockResolvedValue({ data: { environmentUpdate: mutationResult } }),
- ],
+ [getEnvironment, getEnvironmentQuery],
+ [getEnvironmentWithNamespace, getEnvironmentWithNamespaceQuery],
+ [updateEnvironment, mutationHandler],
];
return createMockApollo(mocks);
};
- const createWrapper = () => {
- wrapper = mountExtended(EditEnvironment, {
- propsData: { environment: { id: '1', name: 'foo', external_url: 'https://foo.example.com' } },
- provide,
- });
- };
-
- const createWrapperWithApollo = async ({ mutationResult = environmentUpdate } = {}) => {
+ const createWrapperWithApollo = async ({
+ mutationHandler = updateEnvironmentSuccess,
+ kubernetesNamespaceForEnvironment = false,
+ } = {}) => {
wrapper = mountExtended(EditEnvironment, {
propsData: { environment: {} },
provide: {
...provide,
glFeatures: {
- environmentSettingsToGraphql: true,
+ kubernetesNamespaceForEnvironment,
},
},
- apolloProvider: createMockApolloProvider(mutationResult),
+ apolloProvider: createMockApolloProvider(mutationHandler),
});
await waitForPromises();
@@ -87,43 +90,46 @@ describe('~/environments/components/edit.vue', () => {
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
- const submitForm = async () => {
- await findExternalUrlInput().setValue(newExternalUrl);
- await findForm().trigger('submit');
- };
-
describe('default', () => {
- beforeEach(async () => {
- await createWrapper();
+ it('performs the environment apollo query', () => {
+ createWrapperWithApollo();
+ expect(getEnvironmentQuery).toHaveBeenCalled();
+ });
+
+ it('renders loading icon when environment query is loading', () => {
+ createWrapperWithApollo();
+ expect(showsLoading()).toBe(true);
});
- it('sets the title to Edit environment', () => {
+ it('sets the title to Edit environment', async () => {
+ await createWrapperWithApollo();
+
const header = wrapper.findByRole('heading', { name: __('Edit environment') });
expect(header.exists()).toBe(true);
});
- it('renders a disabled "Name" field', () => {
- const nameInput = findNameInput();
+ it('renders a disabled "Name" field', async () => {
+ await createWrapperWithApollo();
+ const nameInput = findNameInput();
expect(nameInput.attributes().disabled).toBe('disabled');
expect(nameInput.element.value).toBe(environment.name);
});
- it('renders an "External URL" field', () => {
- const urlInput = findExternalUrlInput();
+ it('renders an "External URL" field', async () => {
+ await createWrapperWithApollo();
+ const urlInput = findExternalUrlInput();
expect(urlInput.element.value).toBe(environment.externalUrl);
});
});
- describe('when environmentSettingsToGraphql feature is enabled', () => {
- describe('when mounted', () => {
- beforeEach(() => {
- createWrapperWithApollo();
- });
- it('renders loading icon when environment query is loading', () => {
- expect(showsLoading()).toBe(true);
- });
+ describe('on submit', () => {
+ it('performs the updateEnvironment apollo mutation', async () => {
+ await createWrapperWithApollo();
+ await findForm().trigger('submit');
+
+ expect(updateEnvironmentSuccess).toHaveBeenCalled();
});
describe('when mutation successful', () => {
@@ -134,28 +140,28 @@ describe('~/environments/components/edit.vue', () => {
it('shows loader after form is submitted', async () => {
expect(showsLoading()).toBe(false);
- await submitForm();
+ await findForm().trigger('submit');
expect(showsLoading()).toBe(true);
});
it('submits the updated environment on submit', async () => {
- await submitForm();
+ await findForm().trigger('submit');
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith(environmentUpdate.environment.path);
+ expect(visitUrl).toHaveBeenCalledWith(environmentUpdateSuccess.environment.path);
});
});
describe('when mutation failed', () => {
beforeEach(async () => {
await createWrapperWithApollo({
- mutationResult: environmentUpdateError,
+ mutationHandler: updateEnvironmentFail,
});
});
it('shows errors on error', async () => {
- await submitForm();
+ await findForm().trigger('submit');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
@@ -164,58 +170,10 @@ describe('~/environments/components/edit.vue', () => {
});
});
- describe('when environmentSettingsToGraphql feature is disabled', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
- createWrapper();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('shows loader after form is submitted', async () => {
- expect(showsLoading()).toBe(false);
-
- mock
- .onPut(provide.updateEnvironmentPath, {
- external_url: newExternalUrl,
- id: environment.id,
- })
- .reply(...[HTTP_STATUS_OK, { path: '/test' }]);
-
- await submitForm();
-
- expect(showsLoading()).toBe(true);
- });
-
- it('submits the updated environment on submit', async () => {
- mock
- .onPut(provide.updateEnvironmentPath, {
- external_url: newExternalUrl,
- id: environment.id,
- })
- .reply(...[HTTP_STATUS_OK, { path: '/test' }]);
-
- await submitForm();
- await waitForPromises();
-
- expect(visitUrl).toHaveBeenCalledWith('/test');
- });
-
- it('shows errors on error', async () => {
- mock
- .onPut(provide.updateEnvironmentPath, {
- external_url: newExternalUrl,
- id: environment.id,
- })
- .reply(...[HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]);
-
- await submitForm();
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
- expect(showsLoading()).toBe(false);
+ describe('when `kubernetesNamespaceForEnvironment` is enabled', () => {
+ it('calls the `getEnvironmentWithNamespace` query', () => {
+ createWrapperWithApollo({ kubernetesNamespaceForEnvironment: true });
+ expect(getEnvironmentWithNamespaceQuery).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index db81c490747..803207bcce8 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
@@ -6,6 +6,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentForm from '~/environments/components/environment_form.vue';
import getUserAuthorizedAgents from '~/environments/graphql/queries/user_authorized_agents.query.graphql';
import createMockApollo from '../__helpers__/mock_apollo_helper';
+import { mockKasTunnelUrl } from './mock_data';
jest.mock('~/lib/utils/csrf');
@@ -15,7 +16,10 @@ const DEFAULT_PROPS = {
cancelPath: '/cancel',
};
-const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd' };
+const PROVIDE = {
+ protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd',
+ kasTunnelUrl: mockKasTunnelUrl,
+};
const userAccessAuthorizedAgents = [
{ agent: { id: '1', name: 'agent-1' } },
{ agent: { id: '2', name: 'agent-2' } },
@@ -24,6 +28,10 @@ const userAccessAuthorizedAgents = [
describe('~/environments/components/form.vue', () => {
let wrapper;
+ const getNamespacesQueryResult = jest
+ .fn()
+ .mockReturnValue([{ metadata: { name: 'default' } }, { metadata: { name: 'agent' } }]);
+
const createWrapper = (propsData = {}, options = {}) =>
mountExtended(EnvironmentForm, {
provide: PROVIDE,
@@ -34,37 +42,57 @@ describe('~/environments/components/form.vue', () => {
},
});
- const createWrapperWithApollo = ({ propsData = {} } = {}) => {
+ const createWrapperWithApollo = ({
+ propsData = {},
+ kubernetesNamespaceForEnvironment = false,
+ queryResult = null,
+ } = {}) => {
Vue.use(VueApollo);
+ const requestHandlers = [
+ [
+ getUserAuthorizedAgents,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ userAccessAuthorizedAgents: { nodes: userAccessAuthorizedAgents },
+ },
+ },
+ }),
+ ],
+ ];
+
+ const mockResolvers = {
+ Query: {
+ k8sNamespaces: queryResult || getNamespacesQueryResult,
+ },
+ };
+
return mountExtended(EnvironmentForm, {
provide: {
...PROVIDE,
glFeatures: {
- environmentSettingsToGraphql: true,
+ kubernetesNamespaceForEnvironment,
},
},
propsData: {
...DEFAULT_PROPS,
...propsData,
},
- apolloProvider: createMockApollo([
- [
- getUserAuthorizedAgents,
- jest.fn().mockResolvedValue({
- data: {
- project: {
- id: '1',
- userAccessAuthorizedAgents: { nodes: userAccessAuthorizedAgents },
- },
- },
- }),
- ],
- ]),
+ apolloProvider: createMockApollo(requestHandlers, mockResolvers),
});
};
- const findAgentSelector = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAgentSelector = () => wrapper.findByTestId('agent-selector');
+ const findNamespaceSelector = () => wrapper.findByTestId('namespace-selector');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const selectAgent = async () => {
+ findAgentSelector().vm.$emit('shown');
+ await waitForPromises();
+ await findAgentSelector().vm.$emit('select', '2');
+ };
describe('default', () => {
beforeEach(() => {
@@ -207,12 +235,6 @@ describe('~/environments/components/form.vue', () => {
expect(urlInput.element.value).toBe('https://example.com');
});
- });
-
- describe('when `environmentSettingsToGraphql feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = createWrapperWithApollo();
- });
it('renders an agent selector listbox', () => {
expect(findAgentSelector().props()).toMatchObject({
@@ -224,6 +246,12 @@ describe('~/environments/components/form.vue', () => {
items: [],
});
});
+ });
+
+ describe('agent selector', () => {
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo();
+ });
it('sets the items prop of the agent selector after fetching the list', async () => {
findAgentSelector().vm.$emit('shown');
@@ -253,24 +281,146 @@ describe('~/environments/components/form.vue', () => {
});
it('updates agent selector field with the name of selected agent', async () => {
- findAgentSelector().vm.$emit('shown');
- await waitForPromises();
- await findAgentSelector().vm.$emit('select', '2');
+ await selectAgent();
expect(findAgentSelector().props('toggleText')).toBe('agent-2');
});
it('emits changes to the clusterAgentId', async () => {
- findAgentSelector().vm.$emit('shown');
- await waitForPromises();
- await findAgentSelector().vm.$emit('select', '2');
+ await selectAgent();
expect(wrapper.emitted('change')).toEqual([
- [{ name: '', externalUrl: '', clusterAgentId: '2' }],
+ [{ name: '', externalUrl: '', clusterAgentId: '2', kubernetesNamespace: null }],
]);
});
});
+ describe('namespace selector', () => {
+ it("doesn't render namespace selector if `kubernetesNamespaceForEnvironment` feature flag is disabled", () => {
+ wrapper = createWrapperWithApollo();
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
+
+ describe('when `kubernetesNamespaceForEnvironment` feature flag is enabled', () => {
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo({
+ kubernetesNamespaceForEnvironment: true,
+ });
+ });
+
+ it("doesn't render namespace selector by default", () => {
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
+
+ describe('when the agent was selected', () => {
+ beforeEach(async () => {
+ await selectAgent();
+ });
+
+ it('renders namespace selector', () => {
+ expect(findNamespaceSelector().exists()).toBe(true);
+ });
+
+ it('requests the kubernetes namespaces with the correct configuration', async () => {
+ const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: {
+ 'GitLab-Agent-Id': 2,
+ },
+ withCredentials: true,
+ },
+ };
+
+ await waitForPromises();
+
+ expect(getNamespacesQueryResult).toHaveBeenCalledWith(
+ {},
+ { configuration },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('sets the loading prop while fetching the list', async () => {
+ expect(findNamespaceSelector().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('loading')).toBe(false);
+ });
+
+ it('renders a list of available namespaces', async () => {
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { text: 'default', value: 'default' },
+ { text: 'agent', value: 'agent' },
+ ]);
+ });
+
+ it('filters the namespaces list on user search', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('search', 'default');
+
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { value: 'default', text: 'default' },
+ ]);
+ });
+
+ it('updates namespace selector field with the name of selected namespace', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+ });
+
+ it('emits changes to the kubernetesNamespace', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(wrapper.emitted('change')[1]).toEqual([
+ { name: '', externalUrl: '', kubernetesNamespace: 'agent' },
+ ]);
+ });
+
+ it('clears namespace selector when another agent was selected', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+
+ await findAgentSelector().vm.$emit('select', '1');
+ expect(findNamespaceSelector().props('toggleText')).toBe(
+ EnvironmentForm.i18n.namespaceHelpText,
+ );
+ });
+ });
+
+ describe('when cannot connect to the cluster', () => {
+ const error = new Error('Error from the cluster_client API');
+
+ beforeEach(async () => {
+ wrapper = createWrapperWithApollo({
+ kubernetesNamespaceForEnvironment: true,
+ queryResult: jest.fn().mockRejectedValueOnce(error),
+ });
+
+ await selectAgent();
+ await waitForPromises();
+ });
+
+ it("doesn't render the namespace selector", () => {
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
+
+ it('renders an alert', () => {
+ expect(findAlert().text()).toBe('Error from the cluster_client API');
+ });
+ });
+ });
+ });
+
describe('when environment has an associated agent', () => {
const environmentWithAgent = {
...DEFAULT_PROPS.environment,
@@ -280,11 +430,46 @@ describe('~/environments/components/form.vue', () => {
beforeEach(() => {
wrapper = createWrapperWithApollo({
propsData: { environment: environmentWithAgent },
+ kubernetesNamespaceForEnvironment: true,
});
});
it('updates agent selector field with the name of the associated agent', () => {
expect(findAgentSelector().props('toggleText')).toBe('agent-1');
});
+
+ it('renders namespace selector', async () => {
+ await waitForPromises();
+ expect(findNamespaceSelector().exists()).toBe(true);
+ });
+
+ it('renders a list of available namespaces', async () => {
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { text: 'default', value: 'default' },
+ { text: 'agent', value: 'agent' },
+ ]);
+ });
+ });
+
+ describe('when environment has an associated kubernetes namespace', () => {
+ const environmentWithAgentAndNamespace = {
+ ...DEFAULT_PROPS.environment,
+ clusterAgent: { id: '1', name: 'agent-1' },
+ clusterAgentId: '1',
+ kubernetesNamespace: 'default',
+ };
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo({
+ propsData: { environment: environmentWithAgentAndNamespace },
+ kubernetesNamespaceForEnvironment: true,
+ });
+ });
+
+ it('updates namespace selector with the name of the associated namespace', async () => {
+ await waitForPromises();
+ expect(findNamespaceSelector().props('toggleText')).toBe('default');
+ });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 91268ade1e9..c2eafa5f51e 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -909,3 +909,8 @@ export const k8sWorkloadsMock = {
JobList: [completedJob, completedJob, failedJob],
CronJobList: [completedCronJob, suspendedCronJob, failedCronJob],
};
+
+export const k8sNamespacesMock = [
+ { metadata: { name: 'default' } },
+ { metadata: { name: 'agent' } },
+];
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index edffc00e185..be210ed619e 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -12,6 +12,7 @@ import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.quer
import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql';
import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql';
import { TEST_HOST } from 'helpers/test_constants';
+import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
import {
environmentsApp,
resolvedEnvironmentsApp,
@@ -20,6 +21,7 @@ import {
resolvedFolder,
k8sPodsMock,
k8sServicesMock,
+ k8sNamespacesMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -319,6 +321,50 @@ describe('~/frontend/environments/graphql/resolvers', () => {
);
});
});
+ describe('k8sNamespaces', () => {
+ const mockNamespacesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sNamespacesMock,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1Namespace')
+ .mockImplementation(mockNamespacesListFn);
+ });
+
+ it('should request all namespaces from the cluster_client library', async () => {
+ const namespaces = await mockResolvers.Query.k8sNamespaces(null, { configuration });
+
+ expect(mockNamespacesListFn).toHaveBeenCalled();
+
+ expect(namespaces).toEqual(k8sNamespacesMock);
+ });
+ it.each([
+ ['Unauthorized', CLUSTER_AGENT_ERROR_MESSAGES.unauthorized],
+ ['Forbidden', CLUSTER_AGENT_ERROR_MESSAGES.forbidden],
+ ['Not found', CLUSTER_AGENT_ERROR_MESSAGES['not found']],
+ ['Unknown', CLUSTER_AGENT_ERROR_MESSAGES.other],
+ ])(
+ 'should throw an error if the API call fails with the reason "%s"',
+ async (reason, message) => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1Namespace').mockRejectedValue({
+ response: {
+ data: {
+ reason,
+ },
+ },
+ });
+
+ await expect(mockResolvers.Query.k8sNamespaces(null, { configuration })).rejects.toThrow(
+ message,
+ );
+ },
+ );
+ });
describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index eb6990ba8a8..387bc31c9aa 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -13,6 +13,7 @@ import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql';
+import getEnvironmentClusterAgentWithNamespace from '~/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql';
import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
@@ -21,6 +22,7 @@ Vue.use(VueApollo);
describe('~/environments/components/new_environment_item.vue', () => {
let wrapper;
let queryResponseHandler;
+ let queryWithNamespaceResponseHandler;
const projectPath = '/1';
@@ -37,7 +39,21 @@ describe('~/environments/components/new_environment_item.vue', () => {
},
};
queryResponseHandler = jest.fn().mockResolvedValue(response);
- return createMockApollo([[getEnvironmentClusterAgent, queryResponseHandler]]);
+ queryWithNamespaceResponseHandler = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: response.data.project.id,
+ environment: {
+ ...response.data.project.environment,
+ kubernetesNamespace: 'default',
+ },
+ },
+ },
+ });
+ return createMockApollo([
+ [getEnvironmentClusterAgent, queryResponseHandler],
+ [getEnvironmentClusterAgentWithNamespace, queryWithNamespaceResponseHandler],
+ ]);
};
const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
@@ -521,11 +537,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('should request agent data when the environment is visible if the feature flag is enabled', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
- provideData: {
- glFeatures: {
- kasUserAccessProject: true,
- },
- },
apolloProvider: createApolloProvider(agent),
});
@@ -537,45 +548,62 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
});
- it('should render if the feature flag is enabled and the environment has an agent associated', async () => {
+ it('should request agent data with kubernetes namespace when `kubernetesNamespaceForEnvironment` feature flag is enabled', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
- kasUserAccessProject: true,
+ kubernetesNamespaceForEnvironment: true,
},
},
apolloProvider: createApolloProvider(agent),
});
await expandCollapsedSection();
- await waitForPromises();
- expect(findKubernetesOverview().props()).toMatchObject({
- clusterAgent: agent,
+ expect(queryWithNamespaceResponseHandler).toHaveBeenCalledWith({
+ environmentName: resolvedEnvironment.name,
+ projectFullPath: projectPath,
});
});
- it('should not render if the feature flag is not enabled', async () => {
+ it('should render if the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
apolloProvider: createApolloProvider(agent),
});
await expandCollapsedSection();
+ await waitForPromises();
- expect(queryResponseHandler).not.toHaveBeenCalled();
- expect(findKubernetesOverview().exists()).toBe(false);
+ expect(findKubernetesOverview().props()).toMatchObject({
+ clusterAgent: agent,
+ });
});
- it('should not render if the environment has no agent object', async () => {
+ it('should render with the namespace if `kubernetesNamespaceForEnvironment` feature flag is enabled and the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
- kasUserAccessProject: true,
+ kubernetesNamespaceForEnvironment: true,
},
},
+ apolloProvider: createApolloProvider(agent),
+ });
+
+ await expandCollapsedSection();
+ await waitForPromises();
+
+ expect(findKubernetesOverview().props()).toMatchObject({
+ clusterAgent: agent,
+ namespace: 'default',
+ });
+ });
+
+ it('should not render if the environment has no agent object', async () => {
+ wrapper = createWrapper({
+ propsData: { environment: resolvedEnvironment },
apolloProvider: createApolloProvider(),
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index 749e4e5caa4..30cd9265d0d 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -1,5 +1,4 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -7,8 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import createEnvironment from '~/environments/graphql/mutations/create_environment.mutation.graphql';
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';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
@@ -16,9 +13,6 @@ import createMockApollo from '../__helpers__/mock_apollo_helper';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
-const newName = 'test';
-const newExternalUrl = 'https://google.ca';
-
const provide = {
projectEnvironmentsPath: '/projects/environments',
projectPath: '/path/to/project',
@@ -32,7 +26,6 @@ const environmentCreateError = {
describe('~/environments/components/new.vue', () => {
let wrapper;
- let mock;
const createMockApolloProvider = (mutationResult) => {
Vue.use(VueApollo);
@@ -47,29 +40,13 @@ describe('~/environments/components/new.vue', () => {
const createWrapperWithApollo = async (mutationResult = environmentCreate) => {
wrapper = mountExtended(NewEnvironment, {
- provide: {
- ...provide,
- glFeatures: {
- environmentSettingsToGraphql: true,
- },
- },
+ provide,
apolloProvider: createMockApolloProvider(mutationResult),
});
await waitForPromises();
};
- const createWrapperWithAxios = () => {
- wrapper = mountExtended(NewEnvironment, {
- provide: {
- ...provide,
- glFeatures: {
- environmentSettingsToGraphql: false,
- },
- },
- });
- };
-
const findNameInput = () => wrapper.findByLabelText(__('Name'));
const findExternalUrlInput = () => wrapper.findByLabelText(__('External URL'));
const findForm = () => wrapper.findByRole('form', { name: __('New environment') });
@@ -84,7 +61,7 @@ describe('~/environments/components/new.vue', () => {
describe('default', () => {
beforeEach(() => {
- createWrapperWithAxios();
+ createWrapperWithApollo();
});
it('sets the title to New environment', () => {
@@ -103,93 +80,36 @@ describe('~/environments/components/new.vue', () => {
});
});
- describe('when environmentSettingsToGraphql feature is enabled', () => {
- describe('when mutation successful', () => {
- beforeEach(() => {
- createWrapperWithApollo();
- });
-
- it('shows loader after form is submitted', async () => {
- expect(showsLoading()).toBe(false);
-
- await submitForm();
-
- expect(showsLoading()).toBe(true);
- });
-
- it('submits the new environment on submit', async () => {
- submitForm();
- await waitForPromises();
-
- expect(visitUrl).toHaveBeenCalledWith('path/to/environment');
- });
- });
-
- describe('when failed', () => {
- beforeEach(async () => {
- createWrapperWithApollo(environmentCreateError);
- submitForm();
- await waitForPromises();
- });
-
- it('shows errors on error', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
- expect(showsLoading()).toBe(false);
- });
- });
- });
-
- describe('when environmentSettingsToGraphql feature is disabled', () => {
+ describe('when mutation successful', () => {
beforeEach(() => {
- mock = new MockAdapter(axios);
- createWrapperWithAxios();
- });
-
- afterEach(() => {
- mock.restore();
+ createWrapperWithApollo();
});
it('shows loader after form is submitted', async () => {
expect(showsLoading()).toBe(false);
- mock
- .onPost(provide.projectEnvironmentsPath, {
- name: newName,
- external_url: newExternalUrl,
- })
- .reply(HTTP_STATUS_OK, { path: '/test' });
-
await submitForm();
expect(showsLoading()).toBe(true);
});
it('submits the new environment on submit', async () => {
- mock
- .onPost(provide.projectEnvironmentsPath, {
- name: newName,
- external_url: newExternalUrl,
- })
- .reply(HTTP_STATUS_OK, { path: '/test' });
-
- await submitForm();
+ submitForm();
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('/test');
+ expect(visitUrl).toHaveBeenCalledWith('path/to/environment');
});
+ });
- it('shows errors on error', async () => {
- mock
- .onPost(provide.projectEnvironmentsPath, {
- name: newName,
- external_url: newExternalUrl,
- })
- .reply(HTTP_STATUS_BAD_REQUEST, { message: ['name taken'] });
-
- await submitForm();
+ describe('when failed', () => {
+ beforeEach(async () => {
+ createWrapperWithApollo(environmentCreateError);
+ submitForm();
await waitForPromises();
+ });
- expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
+ it('display errors', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
});
});
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index c9238c4b636..6ef34504da7 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -6,6 +6,9 @@ import {
GlFormInput,
GlAlert,
GlSprintf,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -46,7 +49,13 @@ describe('ErrorDetails', () => {
function mountComponent({ integratedErrorTrackingEnabled = false } = {}) {
wrapper = shallowMount(ErrorDetails, {
- stubs: { GlButton, GlSprintf },
+ stubs: {
+ GlButton,
+ GlSprintf,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
+ },
store,
mocks,
propsData: {
diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb
index 9c22ff176ff..e69287c879b 100644
--- a/spec/frontend/fixtures/groups.rb
+++ b/spec/frontend/fixtures/groups.rb
@@ -2,24 +2,39 @@
require 'spec_helper'
-RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do
+RSpec.describe 'Groups (JavaScript fixtures)', feature_category: :groups_and_projects do
+ include ApiHelpers
include JavaScriptFixturesHelpers
- let(:user) { create(:user) }
- let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre') }
+ let_it_be(:projects) { create_list(:project, 2, namespace: group) }
- before do
- group.add_owner(user)
- sign_in(user)
- end
+ describe GroupsController, '(JavaScript fixtures)', type: :controller do
+ render_views
- render_views
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
- describe GroupsController, '(JavaScript fixtures)', type: :controller do
it 'groups/edit.html' do
get :edit, params: { id: group }
expect(response).to be_successful
end
end
+
+ describe API::Groups, '(JavaScript fixtures)', type: :request do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'api/groups/projects/get.json' do
+ get api("/groups/#{group.id}/projects", user)
+
+ expect(response).to be_successful
+ end
+ end
end
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index e85e683b599..73594ddf686 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -105,7 +105,6 @@ RSpec.describe GraphQL::Query, type: :request do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
- let_it_be(:issue_type) { 'issue' }
before_all do
project.add_reporter(user)
@@ -128,8 +127,7 @@ RSpec.describe GraphQL::Query, type: :request do
title: '15.2',
start_date: Date.new(2020, 7, 1),
due_date: Date.new(2020, 7, 30)
- ),
- issue_type: issue_type
+ )
)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s })
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
deleted file mode 100644
index 036ce9eea3a..00000000000
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
- include MetricsDashboardHelpers
-
- let_it_be(:user) { create(:user) }
- let_it_be(:namespace) { create(:namespace, name: 'monitoring') }
- let_it_be(:project) { project_with_dashboard_namespace('.gitlab/dashboards/test.yml', nil, namespace: namespace) }
- let_it_be(:environment) { create(:environment, id: 1, project: project) }
- let_it_be(:params) { { environment: environment } }
-
- controller(::ApplicationController) do
- include MetricsDashboard
- end
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- sign_in(user)
- project.add_maintainer(user)
-
- allow(controller).to receive(:project).and_return(project)
- allow(controller).to receive(:environment).and_return(environment)
- allow(controller)
- .to receive(:metrics_dashboard_params)
- .and_return(params)
- end
-
- after do
- remove_repository(project)
- end
-
- it 'metrics_dashboard/environment_metrics_dashboard.json' do
- routes.draw { get "metrics_dashboard" => "anonymous#metrics_dashboard" }
-
- response = get :metrics_dashboard, format: :json
-
- expect(response).to be_successful
- end
-end
diff --git a/spec/frontend/fixtures/milestones.rb b/spec/frontend/fixtures/milestones.rb
deleted file mode 100644
index 5e39dcf190a..00000000000
--- a/spec/frontend/fixtures/milestones.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do
- include JavaScriptFixturesHelpers
-
- let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
- let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') }
-
- render_views
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- after do
- remove_repository(project)
- end
-
- it 'milestones/new-milestone.html' do
- get :new, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
-
- expect(response).to be_successful
- end
-
- private
-
- def render_milestone(milestone)
- get :show, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: milestone.to_param
- }
-
- expect(response).to be_successful
- end
-end
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 3bfe9113e83..7bba7910b87 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -63,6 +63,12 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+ it "#{fixtures_path}#{get_pipeline_schedules_query}.single.json" do
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, ids: pipeline_schedule_populated.id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do
guest = create(:user)
project.add_guest(user)
diff --git a/spec/frontend/fixtures/static/line_highlighter.html b/spec/frontend/fixtures/static/line_highlighter.html
index 1667097bc3b..4e1795dfcfa 100644
--- a/spec/frontend/fixtures/static/line_highlighter.html
+++ b/spec/frontend/fixtures/static/line_highlighter.html
@@ -1,154 +1,79 @@
<div class="file-holder">
<div class="file-content">
<div class="line-numbers">
-<a data-line-number="1" href="#L1" id="L1">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="1" href="#L1" id="L1">
1
</a>
-<a data-line-number="2" href="#L2" id="L2">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="2" href="#L2" id="L2">
2
</a>
-<a data-line-number="3" href="#L3" id="L3">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="3" href="#L3" id="L3">
3
</a>
-<a data-line-number="4" href="#L4" id="L4">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="4" href="#L4" id="L4">
4
</a>
-<a data-line-number="5" href="#L5" id="L5">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="5" href="#L5" id="L5">
5
</a>
<a data-line-number="6" href="#L6" id="L6">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
6
</a>
<a data-line-number="7" href="#L7" id="L7">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
7
</a>
<a data-line-number="8" href="#L8" id="L8">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
8
</a>
<a data-line-number="9" href="#L9" id="L9">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
9
</a>
<a data-line-number="10" href="#L10" id="L10">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
10
</a>
<a data-line-number="11" href="#L11" id="L11">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
11
</a>
<a data-line-number="12" href="#L12" id="L12">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
12
</a>
<a data-line-number="13" href="#L13" id="L13">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
13
</a>
<a data-line-number="14" href="#L14" id="L14">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
14
</a>
<a data-line-number="15" href="#L15" id="L15">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
15
</a>
<a data-line-number="16" href="#L16" id="L16">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
16
</a>
<a data-line-number="17" href="#L17" id="L17">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
17
</a>
<a data-line-number="18" href="#L18" id="L18">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
18
</a>
<a data-line-number="19" href="#L19" id="L19">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
19
</a>
<a data-line-number="20" href="#L20" id="L20">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
20
</a>
<a data-line-number="21" href="#L21" id="L21">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
21
</a>
<a data-line-number="22" href="#L22" id="L22">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
22
</a>
<a data-line-number="23" href="#L23" id="L23">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
23
</a>
<a data-line-number="24" href="#L24" id="L24">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
24
</a>
<a data-line-number="25" href="#L25" id="L25">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
25
</a>
</div>
diff --git a/spec/frontend/fixtures/static/textarea.html b/spec/frontend/fixtures/static/textarea.html
new file mode 100644
index 00000000000..68d5a0f2d4d
--- /dev/null
+++ b/spec/frontend/fixtures/static/textarea.html
@@ -0,0 +1,27 @@
+<body>
+<meta charset="utf-8">
+<title>Document with Textarea</title>
+<form class="milestone-form common-note-form js-quick-submit js-requires-input" id="new_milestone"
+ action="http://test.host/frontend-fixtures/milestones-project/-/milestones"
+ accept-charset="UTF-8" method="post">
+ <div class="form-group">
+ <div class="md-write-holder">
+ <div class="zen-backdrop">
+ <textarea class="note-textarea js-gfm-input js-autosize markdown-area"
+ placeholder="Write milestone description..." dir="auto"
+ data-supports-quick-actions="false" data-supports-autocomplete="true"
+ data-qa-selector="milestone_description_field" data-autofocus="false"
+ name="milestone[description]"
+ id="milestone_description"></textarea>
+ <a class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
+ href="#">
+ <svg class="s16" data-testid="minimize-icon">
+ <use href="http://test.host/assets/icons-b8c5a9711f73b1de3c81754da0aca72f43b0e6844aa06dd03092b601a493f45b.svg#minimize"></use>
+ </svg>
+ </a>
+ </div>
+ </div>
+ </div>
+</form>
+
+</body>
diff --git a/spec/frontend/fixtures/timezones.rb b/spec/frontend/fixtures/timezones.rb
index 2393f4e797d..f04e647c8eb 100644
--- a/spec/frontend/fixtures/timezones.rb
+++ b/spec/frontend/fixtures/timezones.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- include TimeZoneHelper
+ include described_class
let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json }
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
index 89bffea7e4c..800a9af194e 100644
--- a/spec/frontend/fixtures/users.rb
+++ b/spec/frontend/fixtures/users.rb
@@ -7,7 +7,8 @@ RSpec.describe 'Users (JavaScript fixtures)', feature_category: :user_profile do
include ApiHelpers
let_it_be(:followers) { create_list(:user, 5) }
- let_it_be(:user) { create(:user, followers: followers) }
+ let_it_be(:followees) { create_list(:user, 5) }
+ let_it_be(:user) { create(:user, followers: followers, followees: followees) }
describe API::Users, '(JavaScript fixtures)', type: :request do
it 'api/users/followers/get.json' do
@@ -15,6 +16,12 @@ RSpec.describe 'Users (JavaScript fixtures)', feature_category: :user_profile do
expect(response).to be_successful
end
+
+ it 'api/users/following/get.json' do
+ get api("/users/#{user.id}/following", user)
+
+ expect(response).to be_successful
+ end
end
describe UsersController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js
index 5e15b4b33e0..6563daee6c3 100644
--- a/spec/frontend/frequent_items/mock_data.js
+++ b/spec/frontend/frequent_items/mock_data.js
@@ -69,7 +69,7 @@ export const mockFrequentGroups = [
},
];
-export const mockSearchedGroups = [mockRawGroup];
+export const mockSearchedGroups = { data: [mockRawGroup] };
export const mockProcessedSearchedGroups = [mockGroup];
export const mockProject = {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 73284fbe5e5..2d19c9871b6 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -866,7 +866,7 @@ describe('GfmAutoComplete', () => {
it('should return a correct template', () => {
const actual = GfmAutoComplete.Emoji.templateFunction(mockItem);
const glEmojiTag = `<gl-emoji data-name="${mockItem.emoji.name}"></gl-emoji>`;
- const expected = `<li>${mockItem.fieldValue} ${glEmojiTag}</li>`;
+ const expected = `<li>${glEmojiTag} ${mockItem.fieldValue}</li>`;
expect(actual).toBe(expected);
});
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
index f1ed32a5f79..b1a1d2d1372 100644
--- a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
@@ -1,6 +1,7 @@
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import { sprintf } from '~/locale';
import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
import * as utils from '~/gitlab_version_check/utils';
@@ -14,6 +15,8 @@ import {
describe('SecurityPatchUpgradeAlertModal', () => {
let wrapper;
let trackingSpy;
+ const hideMock = jest.fn();
+ const { i18n } = SecurityPatchUpgradeAlertModal;
const defaultProps = {
currentVersion: '11.1.1',
@@ -28,14 +31,20 @@ describe('SecurityPatchUpgradeAlertModal', () => {
...props,
},
stubs: {
- GlModal,
GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: hideMock,
+ },
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
},
});
};
afterEach(() => {
unmockTracking();
+ hideMock.mockClear();
});
const expectDispatchedTracking = (action, label) => {
@@ -63,12 +72,12 @@ describe('SecurityPatchUpgradeAlertModal', () => {
});
it('renders the modal title correctly', () => {
- expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle);
+ expect(findGlModalTitle().text()).toBe(i18n.modalTitle);
});
it('renders modal body without suggested versions', () => {
expect(findGlModalBody().text()).toBe(
- sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, {
+ sprintf(i18n.modalBodyNoStableVersions, {
currentVersion: defaultProps.currentVersion,
}),
);
@@ -90,7 +99,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
describe('Learn more link', () => {
it('renders with correct text and link', () => {
- expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore);
+ expect(findGlLink().text()).toBe(i18n.learnMore);
expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
});
@@ -102,12 +111,8 @@ describe('SecurityPatchUpgradeAlertModal', () => {
});
describe('Remind me button', () => {
- beforeEach(() => {
- wrapper.vm.$refs.alertModal.hide = jest.fn();
- });
-
it('renders with correct text', () => {
- expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText);
+ expect(findGlRemindButton().text()).toBe(i18n.secondaryButtonText);
});
it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
@@ -126,13 +131,13 @@ describe('SecurityPatchUpgradeAlertModal', () => {
it('hides the modal', async () => {
await findGlRemindButton().vm.$emit('click');
- expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled();
+ expect(hideMock).toHaveBeenCalled();
});
});
describe('Upgrade button', () => {
it('renders with correct text and link', () => {
- expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText);
+ expect(findGlUpgradeButton().text()).toBe(i18n.primaryButtonText);
expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
});
@@ -160,7 +165,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
it('renders modal body with suggested versions', () => {
expect(findGlModalBody().text()).toBe(
- sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, {
+ sprintf(i18n.modalBodyStableVersions, {
currentVersion: defaultProps.currentVersion,
latestStableVersions: latestStableVersions.join(', '),
}),
@@ -176,9 +181,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
});
it('renders modal details', () => {
- expect(findGlModalDetails().text()).toBe(
- sprintf(wrapper.vm.$options.i18n.modalDetails, { details }),
- );
+ expect(findGlModalDetails().text()).toBe(sprintf(i18n.modalDetails, { details }));
});
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index b474745790e..e32c50db8bf 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -93,10 +93,9 @@ describe('AppComponent', () => {
page: 2,
filterGroupsBy: 'git',
sortBy: 'created_desc',
- archived: true,
})
.then(() => {
- expect(getGroupsSpy).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true);
+ expect(getGroupsSpy).toHaveBeenCalledWith(1, 2, 'git', 'created_desc');
});
});
@@ -154,7 +153,6 @@ describe('AppComponent', () => {
filterGroupsBy: 'foobar',
sortBy: null,
updatePagination: true,
- archived: null,
});
return fetchPromise.then(() => {
expect(vm.updateGroups).toHaveBeenCalledWith(mockSearchedGroups, true);
@@ -177,7 +175,6 @@ describe('AppComponent', () => {
page: 2,
filterGroupsBy: null,
sortBy: null,
- archived: true,
});
expect(vm.isLoading).toBe(true);
@@ -186,7 +183,6 @@ describe('AppComponent', () => {
filterGroupsBy: null,
sortBy: null,
updatePagination: true,
- archived: true,
});
return fetchPagePromise.then(() => {
@@ -471,7 +467,7 @@ describe('AppComponent', () => {
it('calls API with expected params', () => {
emitFetchFilteredAndSortedGroups();
- expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort, undefined);
+ expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort);
});
it('updates pagination', () => {
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index ca852f398d0..8db69295ac4 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -10,26 +10,29 @@ import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_pr
import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archived_projects_empty_state.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
+import ArchivedProjectsService from '~/groups/service/archived_projects_service';
import { createRouter } from '~/groups/init_overview_tabs';
import eventHub from '~/groups/event_hub';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
- OVERVIEW_TABS_SORTING_ITEMS,
+ SORTING_ITEM_NAME,
+ SORTING_ITEM_UPDATED,
+ SORTING_ITEM_STARS,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
Vue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
-const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
describe('OverviewTabs', () => {
let wrapper;
let axiosMock;
const defaultProvide = {
+ groupId: '1',
endpoints: {
subgroups_and_projects: '/groups/foobar/-/children.json',
shared: '/groups/foobar/-/shared_projects.json',
@@ -92,7 +95,10 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
store: new GroupsStore({ showSchemaMarkup: true }),
- service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ service: new GroupsService(
+ defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ defaultProvide.initialSort,
+ ),
});
await waitForPromises();
@@ -115,7 +121,10 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SHARED,
store: new GroupsStore(),
- service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
+ service: new GroupsService(
+ defaultProvide.endpoints[ACTIVE_TAB_SHARED],
+ defaultProvide.initialSort,
+ ),
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
@@ -140,7 +149,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_ARCHIVED,
store: new GroupsStore(),
- service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
+ service: new ArchivedProjectsService(defaultProvide.groupId, defaultProvide.initialSort),
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
@@ -219,7 +228,7 @@ describe('OverviewTabs', () => {
it(`pushes expected route when ${tabToClick} tab is clicked`, async () => {
await findTab(tabToClick).trigger('click');
- expect(routerMock.push).toHaveBeenCalledWith(expectedRoute);
+ expect(routerMock.push).toHaveBeenCalledWith(expect.objectContaining(expectedRoute));
});
});
@@ -304,6 +313,52 @@ describe('OverviewTabs', () => {
sharedAssertions({ search: '', sort: SORTING_ITEM_UPDATED.asc });
});
+ describe('when tab is changed', () => {
+ describe('when selected sort is supported', () => {
+ beforeEach(async () => {
+ await createComponent({
+ route: {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: 'foo/bar/baz' },
+ query: { sort: SORTING_ITEM_NAME.asc },
+ },
+ });
+ });
+
+ it('adds sort query string', async () => {
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+
+ expect(routerMock.push).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: { sort: SORTING_ITEM_NAME.asc },
+ }),
+ );
+ });
+ });
+
+ describe('when selected sort is not supported', () => {
+ beforeEach(async () => {
+ await createComponent({
+ route: {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: 'foo/bar/baz' },
+ query: { sort: SORTING_ITEM_STARS.asc },
+ },
+ });
+ });
+
+ it('defaults to sorting by name', async () => {
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+
+ expect(routerMock.push).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: { sort: SORTING_ITEM_NAME.asc },
+ }),
+ );
+ });
+ });
+ });
+
describe('when sort direction is changed', () => {
beforeEach(async () => {
await setup();
diff --git a/spec/frontend/groups/service/archived_projects_service_spec.js b/spec/frontend/groups/service/archived_projects_service_spec.js
new file mode 100644
index 00000000000..3aec9d57ee1
--- /dev/null
+++ b/spec/frontend/groups/service/archived_projects_service_spec.js
@@ -0,0 +1,90 @@
+import projects from 'test_fixtures/api/groups/projects/get.json';
+import ArchivedProjectsService from '~/groups/service/archived_projects_service';
+import Api from '~/api';
+
+jest.mock('~/api');
+
+describe('ArchivedProjectsService', () => {
+ const groupId = 1;
+ let service;
+
+ beforeEach(() => {
+ service = new ArchivedProjectsService(groupId, 'name_asc');
+ });
+
+ describe('getGroups', () => {
+ const headers = { 'x-next-page': '2', 'x-page': '1', 'x-per-page': '20' };
+ const page = 2;
+ const query = 'git';
+ const sort = 'created_asc';
+
+ beforeEach(() => {
+ Api.groupProjects.mockResolvedValueOnce({ data: projects, headers });
+ });
+
+ it('returns promise the resolves with formatted project', async () => {
+ await expect(service.getGroups(undefined, page, query, sort)).resolves.toEqual({
+ data: projects.map((project) => {
+ return {
+ id: project.id,
+ name: project.name,
+ full_name: project.name_with_namespace,
+ markdown_description: project.description_html,
+ visibility: project.visibility,
+ avatar_url: project.avatar_url,
+ relative_path: `/${project.path_with_namespace}`,
+ edit_path: null,
+ leave_path: null,
+ can_edit: false,
+ can_leave: false,
+ can_remove: false,
+ type: 'project',
+ permission: null,
+ children: [],
+ parent_id: project.namespace.id,
+ project_count: 0,
+ subgroup_count: 0,
+ number_users_with_delimiter: 0,
+ star_count: project.star_count,
+ updated_at: project.updated_at,
+ marked_for_deletion: project.marked_for_deletion_at !== null,
+ last_activity_at: project.last_activity_at,
+ };
+ }),
+ headers,
+ });
+
+ expect(Api.groupProjects).toHaveBeenCalledWith(groupId, query, {
+ archived: true,
+ page,
+ order_by: 'created_at',
+ sort: 'asc',
+ });
+ });
+
+ describe.each`
+ sortArgument | expectedOrderByParameter | expectedSortParameter
+ ${'name_asc'} | ${'name'} | ${'asc'}
+ ${'name_desc'} | ${'name'} | ${'desc'}
+ ${'created_asc'} | ${'created_at'} | ${'asc'}
+ ${'created_desc'} | ${'created_at'} | ${'desc'}
+ ${'latest_activity_asc'} | ${'last_activity_at'} | ${'asc'}
+ ${'latest_activity_desc'} | ${'last_activity_at'} | ${'desc'}
+ ${undefined} | ${'name'} | ${'asc'}
+ `(
+ 'when the sort argument is $sortArgument',
+ ({ sortArgument, expectedSortParameter, expectedOrderByParameter }) => {
+ it(`calls the API with sort parameter set to ${expectedSortParameter} and order_by parameter set to ${expectedOrderByParameter}`, () => {
+ service.getGroups(undefined, page, query, sortArgument);
+
+ expect(Api.groupProjects).toHaveBeenCalledWith(groupId, query, {
+ archived: true,
+ page,
+ order_by: expectedOrderByParameter,
+ sort: expectedSortParameter,
+ });
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/groups/service/groups_service_spec.js b/spec/frontend/groups/service/groups_service_spec.js
index e037a6df1e2..ef0a7fde70a 100644
--- a/spec/frontend/groups/service/groups_service_spec.js
+++ b/spec/frontend/groups/service/groups_service_spec.js
@@ -7,7 +7,7 @@ describe('GroupsService', () => {
let service;
beforeEach(() => {
- service = new GroupsService(mockEndpoint);
+ service = new GroupsService(mockEndpoint, 'created_asc');
});
describe('getGroups', () => {
@@ -17,17 +17,28 @@ describe('GroupsService', () => {
page: 2,
filter: 'git',
sort: 'created_asc',
- archived: true,
};
- service.getGroups(55, 2, 'git', 'created_asc', true);
+ service.getGroups(55, 2, 'git', 'created_asc');
expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params: { parent_id: 55 } });
- service.getGroups(null, 2, 'git', 'created_asc', true);
+ service.getGroups(null, 2, 'git', 'created_asc');
expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params });
});
+
+ describe('when sort argument is undefined', () => {
+ it('calls API with `initialSort` argument', () => {
+ jest.spyOn(axios, 'get').mockResolvedValue();
+
+ service.getGroups(undefined, 2, 'git', undefined);
+
+ expect(axios.get).toHaveBeenCalledWith(mockEndpoint, {
+ params: { sort: 'created_asc', filter: 'git', page: 2 },
+ });
+ });
+ });
});
describe('leaveGroup', () => {
diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js
index baf3c6f08b2..459ca33ee66 100644
--- a/spec/frontend/header_search/init_spec.js
+++ b/spec/frontend/header_search/init_spec.js
@@ -5,7 +5,6 @@ import initHeaderSearch, { eventHandler, cleanEventListeners } from '~/header_se
describe('Header Search EventListener', () => {
beforeEach(() => {
jest.resetModules();
- jest.restoreAllMocks();
setHTMLFixture(`
<div class="js-header-content">
<div class="header-search-form" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search">
@@ -16,7 +15,6 @@ describe('Header Search EventListener', () => {
afterEach(() => {
resetHTMLFixture();
- jest.clearAllMocks();
});
it('attached event listener', () => {
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index 0ee16f98e7e..fe392a64013 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -1,6 +1,6 @@
-import { mount } from '@vue/test-utils';
import _ from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
import IdeStatusMR from '~/ide/components/ide_status_mr.vue';
import { rightSidebarViews } from '~/ide/constants';
@@ -15,6 +15,8 @@ jest.mock('~/lib/utils/poll');
describe('IdeStatusBar component', () => {
let wrapper;
+ const dummyIntervalId = 1337;
+ let dispatchMock;
const findMRStatus = () => wrapper.findComponent(IdeStatusMR);
@@ -31,14 +33,21 @@ describe('IdeStatusBar component', () => {
...state,
});
- wrapper = mount(IdeStatusBar, { store });
+ wrapper = mountExtended(IdeStatusBar, { store });
+ dispatchMock = jest.spyOn(store, 'dispatch');
};
+ beforeEach(() => {
+ jest.spyOn(window, 'setInterval').mockReturnValue(dummyIntervalId);
+ });
+
+ const findCommitShaLink = () => wrapper.findByTestId('commit-sha-content');
+
describe('default', () => {
it('triggers a setInterval', () => {
mountComponent();
- expect(wrapper.vm.intervalId).not.toBe(null);
+ expect(window.setInterval).toHaveBeenCalledTimes(1);
});
it('renders the statusbar', () => {
@@ -47,34 +56,10 @@ describe('IdeStatusBar component', () => {
expect(wrapper.classes()).toEqual(['ide-status-bar']);
});
- describe('commitAgeUpdate', () => {
- beforeEach(() => {
- mountComponent();
- jest.spyOn(wrapper.vm, 'commitAgeUpdate').mockImplementation(() => {});
- });
-
- afterEach(() => {
- jest.clearAllTimers();
- });
-
- it('gets called every second', () => {
- expect(wrapper.vm.commitAgeUpdate).not.toHaveBeenCalled();
-
- jest.advanceTimersByTime(1000);
-
- expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(1);
-
- jest.advanceTimersByTime(1000);
-
- expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(2);
- });
- });
-
describe('getCommitPath', () => {
it('returns the path to the commit details', () => {
mountComponent();
-
- expect(wrapper.vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
+ expect(findCommitShaLink().attributes('href')).toBe('/commit/abc123de');
});
});
@@ -95,11 +80,10 @@ describe('IdeStatusBar component', () => {
},
};
mountComponent({ pipelines });
- jest.spyOn(wrapper.vm, 'openRightPane').mockImplementation(() => {});
wrapper.find('button').trigger('click');
- expect(wrapper.vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
+ expect(dispatchMock).toHaveBeenCalledWith('rightPane/open', rightSidebarViews.pipelines);
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 6747ec97050..aa99b1cacef 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -158,7 +158,6 @@ describe('RepoEditor', () => {
});
afterEach(() => {
- jest.clearAllMocks();
// 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());
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
index 557626b3cca..b1f192e1d98 100644
--- a/spec/frontend/ide/mock_data.js
+++ b/spec/frontend/ide/mock_data.js
@@ -13,6 +13,7 @@ export const projectData = {
can_push: true,
commit: {
id: '123',
+ short_id: 'abc123de',
},
},
},
@@ -79,6 +80,7 @@ export const jobs = [
path: 'testing',
status: {
icon: 'status_success',
+ group: 'success',
text: 'passed',
},
stage: 'test',
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index a1ca9a69926..bd90832f497 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -1,82 +1,76 @@
-import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import * as groupsApi from '~/api/groups_api';
+import { getGroups } from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
+jest.mock('~/api/groups_api');
+
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
-
-const createComponent = (props = {}) => {
- return mount(GroupSelect, {
- propsData: {
- invalidGroups: [],
- ...props,
- },
- });
+const headers = {
+ 'X-Next-Page': 2,
+ 'X-Page': 1,
+ 'X-Per-Page': 20,
+ 'X-Prev-Page': '',
+ 'X-Total': 40,
+ 'X-Total-Pages': 2,
};
describe('GroupSelect', () => {
let wrapper;
- beforeEach(() => {
- jest.spyOn(groupsApi, 'getGroups').mockResolvedValue(allGroups);
+ const createComponent = (props = {}) => {
+ wrapper = mount(GroupSelect, {
+ propsData: {
+ selectedGroup: {},
+ invalidGroups: [],
+ ...props,
+ },
+ });
+ };
- wrapper = createComponent();
+ beforeEach(() => {
+ getGroups.mockResolvedValueOnce({ data: allGroups, headers });
});
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]');
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxToggle = () => findListbox().find('button[aria-haspopup="listbox"]');
const findAvatarByLabel = (text) =>
wrapper
.findAllComponents(GlAvatarLabeled)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('label') === text);
- it('renders GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search groups',
- });
- });
-
describe('when user types in the search input', () => {
- let resolveApiRequest;
-
- beforeEach(() => {
- jest.spyOn(groupsApi, 'getGroups').mockImplementation(
- () =>
- new Promise((resolve) => {
- resolveApiRequest = resolve;
- }),
- );
-
- findSearchBoxByType().vm.$emit('input', group1.name);
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ getGroups.mockClear();
+ getGroups.mockReturnValueOnce(new Promise(() => {}));
+ findListbox().vm.$emit('search', group1.name);
+ await nextTick();
});
it('calls the API', () => {
- resolveApiRequest({ data: allGroups });
-
- expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
+ expect(getGroups).toHaveBeenCalledWith(group1.name, {
exclude_internal: true,
active: true,
order_by: 'similarity',
});
});
- it('displays loading icon while waiting for API call to resolve', async () => {
- expect(findSearchBoxByType().props('isLoading')).toBe(true);
-
- resolveApiRequest({ data: allGroups });
- await waitForPromises();
-
- expect(findSearchBoxByType().props('isLoading')).toBe(false);
+ it('displays loading icon while waiting for API call to resolve', () => {
+ expect(findListbox().props('searching')).toBe(true);
});
});
describe('avatar label', () => {
- it('includes the correct attributes with name and avatar_url', () => {
+ it('includes the correct attributes with name and avatar_url', async () => {
+ createComponent();
+ await waitForPromises();
+
expect(findAvatarByLabel(group1.full_name).attributes()).toMatchObject({
src: group1.avatar_url,
'entity-id': `${group1.id}`,
@@ -86,8 +80,9 @@ describe('GroupSelect', () => {
});
describe('when filtering out the group from results', () => {
- beforeEach(() => {
- wrapper = createComponent({ invalidGroups: [group1.id] });
+ beforeEach(async () => {
+ createComponent({ invalidGroups: [group1.id] });
+ await waitForPromises();
});
it('does not find an invalid group', () => {
@@ -101,16 +96,93 @@ describe('GroupSelect', () => {
});
describe('when group is selected from the dropdown', () => {
- beforeEach(() => {
- findAvatarByLabel(group1.full_name).trigger('click');
+ beforeEach(async () => {
+ createComponent({
+ selectedGroup: {
+ value: group1.id,
+ id: group1.id,
+ name: group1.full_name,
+ path: group1.path,
+ avatarUrl: group1.avatar_url,
+ },
+ });
+ await waitForPromises();
+ findListbox().vm.$emit('select', group1.id);
+ await nextTick();
});
it('emits `input` event used by `v-model`', () => {
- expect(wrapper.emitted('input')[0][0].id).toEqual(group1.id);
+ expect(wrapper.emitted('input')).toMatchObject([
+ [
+ {
+ value: group1.id,
+ id: group1.id,
+ name: group1.full_name,
+ path: group1.path,
+ avatarUrl: group1.avatar_url,
+ },
+ ],
+ ]);
});
it('sets dropdown toggle text to selected item', () => {
- expect(findDropdownToggle().text()).toBe(group1.full_name);
+ expect(findListboxToggle().text()).toBe(group1.full_name);
+ });
+ });
+
+ describe('infinite scroll', () => {
+ it('sets infinite scroll related props', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findListbox().props()).toMatchObject({
+ infiniteScroll: true,
+ infiniteScrollLoading: false,
+ totalItems: 40,
+ });
+ });
+
+ describe('when `bottom-reached` event is fired', () => {
+ it('indicates new groups are loading and adds them to the listbox', async () => {
+ createComponent();
+ await waitForPromises();
+
+ const infiniteScrollGroup = {
+ id: 3,
+ full_name: 'Infinite scroll group',
+ avatar_url: 'test',
+ };
+
+ getGroups.mockResolvedValueOnce({ data: [infiniteScrollGroup], headers });
+
+ findListbox().vm.$emit('bottom-reached');
+ await nextTick();
+
+ expect(findListbox().props('infiniteScrollLoading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findListbox().props('items')[2]).toMatchObject({
+ value: infiniteScrollGroup.id,
+ id: infiniteScrollGroup.id,
+ name: infiniteScrollGroup.full_name,
+ avatarUrl: infiniteScrollGroup.avatar_url,
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('emits `error` event', async () => {
+ createComponent();
+ await waitForPromises();
+
+ getGroups.mockRejectedValueOnce();
+
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[GroupSelect.i18n.errorMessage]]);
+ });
+ });
});
});
});
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 4f082145562..4136de75545 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Api from '~/api';
@@ -24,6 +24,7 @@ jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
describe('InviteGroupsModal', () => {
let wrapper;
+ const mockToastShow = jest.fn();
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(InviteGroupsModal, {
@@ -39,9 +40,18 @@ describe('InviteGroupsModal', () => {
template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
}),
},
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
});
};
+ afterEach(() => {
+ mockToastShow.mockClear();
+ });
+
const createInviteGroupToProjectWrapper = () => {
createComponent({ isProject: true });
};
@@ -133,7 +143,6 @@ describe('InviteGroupsModal', () => {
createComponent();
triggerGroupSelect(sharedGroup);
- wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockImplementation(
() =>
new Promise((resolve, reject) => {
@@ -167,7 +176,7 @@ describe('InviteGroupsModal', () => {
});
it('displays the successful toastMessage', () => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
+ expect(mockToastShow).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
});
@@ -187,7 +196,7 @@ describe('InviteGroupsModal', () => {
});
it('does not show the toast message on failure', () => {
- expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ expect(mockToastShow).not.toHaveBeenCalled();
});
it('displays the generic error for http server error', () => {
@@ -222,7 +231,6 @@ describe('InviteGroupsModal', () => {
createComponent({ reloadPageOnSubmit: true });
triggerGroupSelect(sharedGroup);
- wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
clickInviteButton();
@@ -238,8 +246,19 @@ describe('InviteGroupsModal', () => {
});
it('does not show the toast message on failure', () => {
- expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ expect(mockToastShow).not.toHaveBeenCalled();
});
});
});
+
+ describe('when group select emits an error event', () => {
+ it('displays error alert', async () => {
+ createComponent();
+
+ findGroupSelect().vm.$emit('error', GroupSelect.i18n.errorMessage);
+ await nextTick();
+
+ expect(wrapper.findComponent(GlAlert).text()).toBe(GroupSelect.i18n.errorMessage);
+ });
+ });
});
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 ff0313cc49e..925534edd7c 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -143,12 +143,19 @@ describe('MembersTokenSelect', () => {
});
describe('when input text is an email', () => {
- it('allows user defined tokens', async () => {
- tokenSelector.vm.$emit('text-input', 'foo@bar.com');
+ it.each`
+ email | result
+ ${'foo@bar.com'} | ${true}
+ ${'foo@bar.com '} | ${false}
+ ${' foo@bar.com'} | ${false}
+ ${'foo@ba r.com'} | ${false}
+ ${'fo o@bar.com'} | ${false}
+ `(`with token creation validation on $email`, async ({ email, result }) => {
+ tokenSelector.vm.$emit('text-input', email);
await nextTick();
- expect(tokenSelector.props('allowUserDefinedTokens')).toBe(true);
+ expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result);
});
});
});
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 7322894164b..bfb0aaa1c67 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -236,23 +236,21 @@ describe('RelatedIssuableItem', () => {
describe('when work item is issue and the related issue title is clicked', () => {
it('does not open', () => {
mountComponent({ props: { workItemType: 'ISSUE' } });
- wrapper.vm.$refs.modal.show = jest.fn();
findTitleLink().vm.$emit('click', { preventDefault: () => {} });
- expect(wrapper.vm.$refs.modal.show).not.toHaveBeenCalled();
+ expect(showModalSpy).not.toHaveBeenCalled();
});
});
describe('when work item is task and the related issue title is clicked', () => {
beforeEach(() => {
mountComponent({ props: { workItemType: 'TASK' } });
- wrapper.vm.$refs.modal.show = jest.fn();
findTitleLink().vm.$emit('click', { preventDefault: () => {} });
});
it('opens', () => {
- expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ expect(showModalSpy).toHaveBeenCalled();
});
it('updates the url params with the work item id', () => {
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index d26f287d90c..0d47595c9e6 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -18,6 +18,8 @@ describe('Merge request status box component', () => {
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
+ ${'epic'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'epic'}
+ ${'epic'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'epic-closed'}
`(
'with issuableType set to "$issuableType" and state set to "$initialState"',
({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index d7e5f9083b0..b9652327e3d 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -4,6 +4,9 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
import { confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
+import { mockTracking } from 'helpers/tracking_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import { getSaveableFormChildren } from './helpers';
jest.mock('~/autosave');
@@ -20,9 +23,12 @@ const createIssuable = (form) => {
};
describe('IssuableForm', () => {
+ let trackingSpy;
let $form;
let instance;
+ useLocalStorageSpy();
+
beforeEach(() => {
setHTMLFixture(`
<form>
@@ -32,6 +38,7 @@ describe('IssuableForm', () => {
</form>
`);
$form = $('form');
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
afterEach(() => {
@@ -266,6 +273,34 @@ describe('IssuableForm', () => {
expect(resetAutosave).toHaveBeenCalled();
});
+ it.each`
+ windowLocation | context | localStorageValue | editorType
+ ${'/gitlab-org/gitlab/-/issues/412699'} | ${'Issue'} | ${'contentEditor'} | ${'editor_type_rich_text_editor'}
+ ${'/gitlab-org/gitlab/-/merge_requests/125979/diffs'} | ${'MergeRequest'} | ${'contentEditor'} | ${'editor_type_rich_text_editor'}
+ ${'/groups/gitlab-org/-/milestones/8/edit'} | ${'Other'} | ${'contentEditor'} | ${'editor_type_rich_text_editor'}
+ ${'/gitlab-org/gitlab/-/issues/412699'} | ${'Issue'} | ${'markdownField'} | ${'editor_type_plain_text_editor'}
+ ${'/gitlab-org/gitlab/-/merge_requests/125979/diffs'} | ${'MergeRequest'} | ${'markdownField'} | ${'editor_type_plain_text_editor'}
+ ${'/groups/gitlab-org/-/milestones/8/edit'} | ${'Other'} | ${'markdownField'} | ${'editor_type_plain_text_editor'}
+ `(
+ 'tracks event on form submit',
+ ({ windowLocation, context, localStorageValue, editorType }) => {
+ setWindowLocation(`${TEST_HOST}/${windowLocation}`);
+ localStorage.setItem('gl-markdown-editor-mode', localStorageValue);
+
+ issueDescription.value = 'sample message';
+
+ createIssuable($form);
+
+ $form.submit();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context,
+ editorType,
+ label: 'editor_tracking',
+ });
+ },
+ );
+
it('prevents form submission when token is present', () => {
issueDescription.value = sensitiveMessage;
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index a7605016039..0596433ce9a 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -26,7 +26,7 @@ describe('Issue Popover', () => {
apolloProvider: createMockApollo([[issueQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
- projectPath: 'foo/bar',
+ namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached title',
},
diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js
index 5b29ecfc0ba..4ed783da853 100644
--- a/spec/frontend/issuable/popover/components/mr_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js
@@ -64,7 +64,7 @@ describe('MR Popover', () => {
apolloProvider: createMockApollo([[mergeRequestQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
- projectPath: 'foo/bar',
+ namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached Title',
},
diff --git a/spec/frontend/issuable/popover/index_spec.js b/spec/frontend/issuable/popover/index_spec.js
index b1aa7f0f0b0..bf9dce4867f 100644
--- a/spec/frontend/issuable/popover/index_spec.js
+++ b/spec/frontend/issuable/popover/index_spec.js
@@ -1,6 +1,6 @@
import { setHTMLFixture } from 'helpers/fixtures';
import * as createDefaultClient from '~/lib/graphql';
-import initIssuablePopovers from '~/issuable/popover/index';
+import initIssuablePopovers, * as popover from '~/issuable/popover/index';
createDefaultClient.default = jest.fn();
@@ -9,6 +9,7 @@ describe('initIssuablePopovers', () => {
let mr2;
let mr3;
let issue1;
+ let workItem1;
beforeEach(() => {
setHTMLFixture(`
@@ -24,30 +25,69 @@ describe('initIssuablePopovers', () => {
<div id="four" class="gfm-issue" title="title" data-iid="1" data-project-path="group/project" data-reference-type="issue">
MR3
</div>
+ <div id="five" class="gfm-work_item" title="title" data-iid="1" data-project-path="group/project" data-reference-type="work_item">
+ MR3
+ </div>
`);
mr1 = document.querySelector('#one');
mr2 = document.querySelector('#two');
mr3 = document.querySelector('#three');
issue1 = document.querySelector('#four');
-
- mr1.addEventListener = jest.fn();
- mr2.addEventListener = jest.fn();
- mr3.addEventListener = jest.fn();
- issue1.addEventListener = jest.fn();
+ workItem1 = document.querySelector('#five');
});
- it('does not add the same event listener twice', () => {
- initIssuablePopovers([mr1, mr1, mr2, issue1]);
+ describe('init function', () => {
+ beforeEach(() => {
+ mr1.addEventListener = jest.fn();
+ mr2.addEventListener = jest.fn();
+ mr3.addEventListener = jest.fn();
+ issue1.addEventListener = jest.fn();
+ workItem1.addEventListener = jest.fn();
+ });
+
+ it('does not add the same event listener twice', () => {
+ initIssuablePopovers([mr1, mr1, mr2, issue1, workItem1]);
+
+ expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
+ expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
+ expect(issue1.addEventListener).toHaveBeenCalledTimes(1);
+ expect(workItem1.addEventListener).toHaveBeenCalledTimes(1);
+ });
- expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
- expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
- expect(issue1.addEventListener).toHaveBeenCalledTimes(1);
+ it('does not add listener if it does not have the necessary data attributes', () => {
+ initIssuablePopovers([mr1, mr2, mr3]);
+
+ expect(mr3.addEventListener).not.toHaveBeenCalled();
+ });
});
- it('does not add listener if it does not have the necessary data attributes', () => {
- initIssuablePopovers([mr1, mr2, mr3]);
+ describe('mount function', () => {
+ const expectedMountObject = {
+ apolloProvider: expect.anything(),
+ iid: '1',
+ namespacePath: 'group/project',
+ title: 'title',
+ };
+
+ beforeEach(() => {
+ jest.spyOn(popover, 'handleIssuablePopoverMount').mockImplementation(jest.fn());
+ });
+
+ it('calls popover mount function with components for Issue, MR, and Work Item', () => {
+ initIssuablePopovers([mr1, issue1, workItem1], popover.handleIssuablePopoverMount);
+
+ [mr1, issue1, workItem1].forEach(async (el) => {
+ await el.dispatchEvent(new Event('mouseenter', { target: el }));
- expect(mr3.addEventListener).not.toHaveBeenCalled();
+ expect(popover.handleIssuablePopoverMount).toHaveBeenCalledWith(
+ expect.objectContaining({
+ ...expectedMountObject,
+ referenceType: el.dataset.referenceType,
+ target: el,
+ }),
+ );
+ });
+ });
});
});
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 e97c0312181..a24bffdd363 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,6 +1,6 @@
import { nextTick } from 'vue';
import { GlIcon, GlCard } from '@gitlab/ui';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
issuable1,
issuable2,
@@ -14,6 +14,7 @@ import {
linkedIssueTypesTextMap,
PathIdSeparator,
} from '~/related_issues/constants';
+import RelatedIssuesList from '~/related_issues/components/related_issues_list.vue';
describe('RelatedIssuesBlock', () => {
let wrapper;
@@ -21,9 +22,10 @@ describe('RelatedIssuesBlock', () => {
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
+ const findAllRelatedIssuesList = () => wrapper.findAllComponents(RelatedIssuesList);
+ const findRelatedIssuesList = (index) => findAllRelatedIssuesList().at(index);
const createComponent = ({
- mountFn = mountExtended,
pathIdSeparator = PathIdSeparator.Issue,
issuableType = TYPE_ISSUE,
canAdmin = false,
@@ -35,7 +37,7 @@ describe('RelatedIssuesBlock', () => {
autoCompleteEpics = true,
slots = '',
} = {}) => {
- wrapper = mountFn(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator,
issuableType,
@@ -76,7 +78,7 @@ describe('RelatedIssuesBlock', () => {
helpPath: '/help/user/project/issues/related_issues',
});
- expect(wrapper.find('.card-title').text()).toContain(titleText);
+ expect(wrapper.findByTestId('card-title').text()).toContain(titleText);
expect(findIssueCountBadgeAddButton().attributes('aria-label')).toBe(addButtonText);
},
);
@@ -94,12 +96,9 @@ describe('RelatedIssuesBlock', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
- createComponent({
- mountFn: shallowMountExtended,
- slots: { 'header-text': headerText },
- });
+ createComponent({ slots: { 'header-text': headerText } });
- expect(wrapper.find('.card-title').html()).toContain(headerText);
+ expect(wrapper.findByTestId('card-title').html()).toContain(headerText);
});
});
@@ -107,10 +106,7 @@ describe('RelatedIssuesBlock', () => {
it('displays header actions slot data', () => {
const headerActions = '<button data-testid="custom-button">custom button</button>';
- createComponent({
- mountFn: shallowMountExtended,
- slots: { 'header-actions': headerActions },
- });
+ createComponent({ slots: { 'header-actions': headerActions } });
expect(wrapper.findByTestId('custom-button').html()).toBe(headerActions);
});
@@ -153,10 +149,6 @@ describe('RelatedIssuesBlock', () => {
});
describe('showCategorizedIssues prop', () => {
- const issueList = () => wrapper.findAll('.js-related-issues-token-list-item');
- const categorizedHeadings = () => wrapper.findAll('h4');
- const headingTextAt = (index) => categorizedHeadings().at(index).text();
-
describe('when showCategorizedIssues=true', () => {
beforeEach(() =>
createComponent({
@@ -166,25 +158,25 @@ describe('RelatedIssuesBlock', () => {
);
it('should render issue tokens items', () => {
- expect(issueList()).toHaveLength(3);
+ expect(findAllRelatedIssuesList()).toHaveLength(3);
});
it('shows "Blocks" heading', () => {
- const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS];
-
- expect(headingTextAt(0)).toBe(blocks);
+ expect(findRelatedIssuesList(0).props('heading')).toBe(
+ linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS],
+ );
});
it('shows "Is blocked by" heading', () => {
- const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY];
-
- expect(headingTextAt(1)).toBe(isBlockedBy);
+ expect(findRelatedIssuesList(1).props('heading')).toBe(
+ linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY],
+ );
});
it('shows "Relates to" heading', () => {
- const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
-
- expect(headingTextAt(2)).toBe(relatesTo);
+ expect(findRelatedIssuesList(2).props('heading')).toBe(
+ linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO],
+ );
});
});
@@ -194,8 +186,9 @@ describe('RelatedIssuesBlock', () => {
showCategorizedIssues: false,
relatedIssues: [issuable1, issuable2, issuable3],
});
- expect(issueList()).toHaveLength(3);
- expect(categorizedHeadings()).toHaveLength(0);
+ expect(findAllRelatedIssuesList()).toHaveLength(1);
+ expect(findRelatedIssuesList(0).props('relatedIssues')).toHaveLength(3);
+ expect(findRelatedIssuesList(0).props('heading')).toBe('');
});
});
});
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 b119c836411..6638f3d6289 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
@@ -1,6 +1,5 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import {
defaultProps,
@@ -17,7 +16,6 @@ import {
import { linkedIssueTypesMap } from '~/related_issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
-import relatedIssuesService from '~/related_issues/services/related_issues_service';
jest.mock('~/alert');
@@ -37,7 +35,7 @@ describe('RelatedIssuesRoot', () => {
});
const createComponent = ({ props = {}, data = {} } = {}) => {
- wrapper = mount(RelatedIssuesRoot, {
+ wrapper = shallowMount(RelatedIssuesRoot, {
propsData: {
...defaultProps,
...props,
@@ -58,14 +56,13 @@ describe('RelatedIssuesRoot', () => {
describe('when "relatedIssueRemoveRequest" event is emitted', () => {
describe('when emitted value is a numerical issue', () => {
beforeEach(async () => {
- jest
- .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
- .mockReturnValue(Promise.reject());
+ mock.onGet(defaultProps.endpoint).reply(HTTP_STATUS_OK, [issuable1]);
await createComponent();
- wrapper.vm.store.setRelatedIssues([issuable1]);
});
- it('removes related issue on API success', async () => {
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/417177
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('removes related issue on API success', async () => {
mock.onDelete(issuable1.referencePath).reply(HTTP_STATUS_OK, { issues: [] });
findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
@@ -91,8 +88,7 @@ describe('RelatedIssuesRoot', () => {
const workItem = `gid://gitlab/WorkItem/${issuable1.id}`;
createComponent({ data: { state: { relatedIssues: [issuable1] } } });
- findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem);
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem);
expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]);
});
@@ -103,8 +99,7 @@ describe('RelatedIssuesRoot', () => {
it('toggles related issues form to visible from hidden', async () => {
createComponent();
- findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(true);
});
@@ -112,24 +107,25 @@ describe('RelatedIssuesRoot', () => {
it('toggles related issues form to hidden from visible', async () => {
createComponent({ data: { isFormVisible: true } });
- findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
});
});
describe('when "pendingIssuableRemoveRequest" event is emitted', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- wrapper.vm.store.setPendingReferences([issuable1.reference]);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1.reference],
+ touchedReference: '',
+ });
});
it('removes pending related issue', async () => {
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(1);
- findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0);
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0);
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
});
@@ -137,33 +133,24 @@ describe('RelatedIssuesRoot', () => {
describe('when "addIssuableFormSubmit" event is emitted', () => {
beforeEach(async () => {
- jest
- .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
- .mockReturnValue(Promise.reject());
await createComponent();
- jest.spyOn(wrapper.vm, 'processAllReferences');
- jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
createAlert.mockClear();
});
- it('processes references before submitting', () => {
+ it('processes references before submitting', async () => {
const input = '#123';
const linkedIssueType = linkedIssueTypesMap.RELATES_TO;
const emitObj = {
pendingReferences: input,
linkedIssueType,
};
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj);
-
- expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
- expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input]);
});
- it('submits zero pending issues as related issue', () => {
- wrapper.vm.store.setPendingReferences([]);
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ it('submits zero pending issues as related issue', async () => {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await waitForPromises();
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
expect(findRelatedIssuesBlock().props('relatedIssues')).toHaveLength(0);
@@ -177,9 +164,11 @@ describe('RelatedIssuesRoot', () => {
status: 'success',
},
});
- wrapper.vm.store.setPendingReferences([issuable1.reference]);
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1],
+ touchedReference: '',
+ });
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
await waitForPromises();
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
@@ -196,9 +185,11 @@ describe('RelatedIssuesRoot', () => {
status: 'success',
},
});
- wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1.reference, issuable2.reference],
+ touchedReference: '',
+ });
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
await waitForPromises();
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
@@ -212,12 +203,15 @@ describe('RelatedIssuesRoot', () => {
const input = '#123';
const message = 'error';
mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_CONFLICT, { message });
- wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1.reference, issuable2.reference],
+ touchedReference: '',
+ });
expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(null);
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
expect(findRelatedIssuesBlock().props('hasError')).toBe(true);
@@ -229,8 +223,7 @@ describe('RelatedIssuesRoot', () => {
beforeEach(() => createComponent({ data: { isFormVisible: true, inputValue: 'foo' } }));
it('hides form and resets input', async () => {
- findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel');
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel');
expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
expect(findRelatedIssuesBlock().props('inputValue')).toBe('');
@@ -243,11 +236,10 @@ describe('RelatedIssuesRoot', () => {
const input = '#123 ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
@@ -256,11 +248,10 @@ describe('RelatedIssuesRoot', () => {
const input = 'asdf/qwer#444 ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
@@ -270,11 +261,10 @@ describe('RelatedIssuesRoot', () => {
const input = `${link} `;
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([link]);
});
@@ -283,11 +273,10 @@ describe('RelatedIssuesRoot', () => {
const input = 'asdf/qwer#444 #12 ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
'asdf/qwer#444',
@@ -299,11 +288,10 @@ describe('RelatedIssuesRoot', () => {
const input = 'something random ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
'something',
@@ -317,11 +305,10 @@ describe('RelatedIssuesRoot', () => {
const input = '23';
createComponent({ props: { pathIdSeparator } });
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('inputValue')).toBe(`${pathIdSeparator}${input}`);
},
@@ -331,15 +318,13 @@ describe('RelatedIssuesRoot', () => {
describe('when "addIssuableFormBlur" event is emitted', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {});
});
- it('adds any references to pending when blurring', () => {
+ it('adds any references to pending when blurring', async () => {
const input = '#123';
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input);
-
- expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([]);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input]);
});
});
});
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 c152a5ef9a8..148c6230b9f 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -54,6 +54,7 @@ describe('IssuesDashboardApp component', () => {
const defaultProvide = {
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
+ autocompleteUsersPath: 'autocomplete/users.json',
calendarPath: 'calendar/path',
dashboardLabelsPath: 'dashboard/labels/path',
dashboardMilestonesPath: 'dashboard/milestones/path',
@@ -120,7 +121,7 @@ describe('IssuesDashboardApp component', () => {
await waitForPromises();
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/391722
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/391722
// eslint-disable-next-line jest/no-disabled-tests
it.skip('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index a61e7ed1e86..8e69213ebba 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -23,6 +23,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
newProjectPath: 'new/project/path',
showNewIssueLink: false,
signInPath: 'sign/in/path',
+ groupId: '',
};
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
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 0e87e5e6595..72bf4826056 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -115,6 +115,7 @@ describe('CE IssuesListApp component', () => {
rssPath: 'rss/path',
showNewIssueLink: true,
signInPath: 'sign/in/path',
+ groupId: '',
};
let defaultQueryResponse = getIssuesQueryResponse;
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 b8adeb24005..f122180a403 100644
--- a/spec/frontend/issues/show/components/delete_issue_modal_spec.js
+++ b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
@@ -37,11 +37,14 @@ describe('DeleteIssueModal component', () => {
});
describe('when "primary" event is emitted', () => {
- let formSubmitSpy;
+ const submitMock = jest.fn();
+ // Mock the form submit method
+ Object.defineProperty(HTMLFormElement.prototype, 'submit', {
+ value: submitMock,
+ });
beforeEach(() => {
wrapper = mountComponent();
- formSubmitSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
findModal().vm.$emit('primary');
});
@@ -50,7 +53,7 @@ describe('DeleteIssueModal component', () => {
});
it('submits the form', () => {
- expect(formSubmitSpy).toHaveBeenCalled();
+ expect(submitMock).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index c7116f380a1..5e329d44acb 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -3,20 +3,19 @@ import DescriptionField from '~/issues/show/components/fields/description.vue';
import eventHub from '~/issues/show/event_hub';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { mockTracking } from 'helpers/tracking_helper';
describe('Description field component', () => {
let wrapper;
+ let trackingSpy;
- const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
-
- const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) =>
- shallowMount(DescriptionField, {
+ const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) => {
+ wrapper = shallowMount(DescriptionField, {
attachTo: document.body,
propsData: {
markdownPreviewPath: '/',
markdownDocsPath: '/',
- quickActionsDocsPath: '/',
value: description,
},
provide: {
@@ -28,90 +27,66 @@ describe('Description field component', () => {
MarkdownField,
},
});
+ };
beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
jest.spyOn(eventHub, '$emit');
- });
-
- it('renders markdown field with description', () => {
- wrapper = mountComponent();
-
- expect(findTextarea().element.value).toBe('test');
- });
-
- it('renders markdown field with a markdown description', () => {
- const markdown = '**test**';
-
- wrapper = mountComponent({ description: markdown });
- expect(findTextarea().element.value).toBe(markdown);
+ mountComponent({ contentEditorOnIssues: true });
});
- it('focuses field when mounted', () => {
- wrapper = mountComponent();
+ it('passes feature flag to the MarkdownEditorComponent', () => {
+ expect(findMarkdownEditor().props('enableContentEditor')).toBe(true);
- expect(document.activeElement).toBe(findTextarea().element);
- });
-
- it('triggers update with meta+enter', () => {
- wrapper = mountComponent();
+ mountComponent({ contentEditorOnIssues: false });
- findTextarea().trigger('keydown.enter', { metaKey: true });
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ expect(findMarkdownEditor().props('enableContentEditor')).toBe(false);
});
- it('triggers update with ctrl+enter', () => {
- wrapper = mountComponent();
-
- findTextarea().trigger('keydown.enter', { ctrlKey: true });
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ it('uses the MarkdownEditor component to edit markdown', () => {
+ expect(findMarkdownEditor().props()).toMatchObject({
+ value: 'test',
+ renderMarkdownPath: '/',
+ autofocus: true,
+ supportsQuickActions: true,
+ markdownDocsPath: '/',
+ enableAutocomplete: true,
+ });
});
- describe('when contentEditorOnIssues feature flag is on', () => {
+ describe.each`
+ testDescription | metaKey | ctrlKey
+ ${'when meta+enter is pressed'} | ${true} | ${false}
+ ${'when ctrl+enter is pressed'} | ${false} | ${true}
+ `('$testDescription', ({ metaKey, ctrlKey }) => {
beforeEach(() => {
- wrapper = mountComponent({ contentEditorOnIssues: true });
- });
-
- it('uses the MarkdownEditor component to edit markdown', () => {
- expect(findMarkdownEditor().props()).toMatchObject({
- value: 'test',
- renderMarkdownPath: '/',
- autofocus: true,
- supportsQuickActions: true,
- quickActionsDocsPath: expect.any(String),
- markdownDocsPath: '/',
- enableAutocomplete: true,
- });
- });
-
- it('triggers update with meta+enter', () => {
findMarkdownEditor().vm.$emit('keydown', {
type: 'keydown',
keyCode: 13,
- metaKey: true,
+ metaKey,
+ ctrlKey,
});
+ });
+ it('triggers update', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
- it('triggers update with ctrl+enter', () => {
- findMarkdownEditor().vm.$emit('keydown', {
- type: 'keydown',
- keyCode: 13,
- ctrlKey: true,
+ it('tracks event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Issue',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
});
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
+ });
- it('emits input event when MarkdownEditor emits input event', () => {
- const markdown = 'markdown';
+ it('emits input event when MarkdownEditor emits input event', () => {
+ const markdown = 'markdown';
- findMarkdownEditor().vm.$emit('input', markdown);
+ findMarkdownEditor().vm.$emit('input', markdown);
- expect(wrapper.emitted('input')).toEqual([[markdown]]);
- });
+ expect(wrapper.emitted('input')).toEqual([[markdown]]);
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 9a503a2d882..8a98b2b702a 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,12 +1,20 @@
import Vue, { nextTick } from 'vue';
-import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
+import {
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ TYPE_INCIDENT,
+ TYPE_ISSUE,
+ TYPE_TEST_CASE,
+ TYPE_ALERT,
+ TYPE_MERGE_REQUEST,
+} 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';
@@ -14,6 +22,7 @@ import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show
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 { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -690,4 +699,27 @@ describe('HeaderActions component', () => {
},
);
});
+
+ describe('issue type text', () => {
+ it.each`
+ issueType | expectedText
+ ${TYPE_ISSUE} | ${'issue'}
+ ${TYPE_INCIDENT} | ${'incident'}
+ ${TYPE_MERGE_REQUEST} | ${'merge request'}
+ ${TYPE_ALERT} | ${'alert'}
+ ${TYPE_TEST_CASE} | ${'test case'}
+ ${'unknown'} | ${'unknown'}
+ `('$issueType', ({ issueType, expectedText }) => {
+ wrapper = mountComponent({
+ movedMrSidebarEnabled: true,
+ props: { issueType, issuableEmailAddress: 'mock-email-address' },
+ });
+
+ expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(
+ `${capitalizeFirstCharacter(expectedText)} actions`,
+ );
+ expect(findDropdownBy('copy-email').text()).toBe(`Copy ${expectedText} email address`);
+ expect(findDesktopDropdownItems().at(0).text()).toBe(`New related ${expectedText}`);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
index 24653a23036..2500c808073 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
@@ -1,5 +1,5 @@
import timezoneMock from 'timezone-mock';
-import { GlIcon, GlDropdown, GlBadge } from '@gitlab/ui';
+import { GlIcon, GlDisclosureDropdown, GlBadge } from '@gitlab/ui';
import { nextTick } from 'vue';
import { timelineItemI18n } from '~/issues/show/components/incidents/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -28,7 +28,7 @@ describe('IncidentTimelineEventList', () => {
const findCommentIcon = () => wrapper.findComponent(GlIcon);
const findEventTime = () => wrapper.findByTestId('event-time');
const findEventTags = () => wrapper.findAllComponents(GlBadge);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
const findEditButton = () => wrapper.findByText(timelineItemI18n.edit);
@@ -85,7 +85,7 @@ describe('IncidentTimelineEventList', () => {
describe('action dropdown', () => {
it('does not show the action dropdown by default', () => {
- expect(findDropdown().exists()).toBe(false);
+ expect(findGlDropdown().exists()).toBe(false);
expect(findDeleteButton().exists()).toBe(false);
});
@@ -100,14 +100,14 @@ describe('IncidentTimelineEventList', () => {
mockEvent: systemGeneratedMockEvent,
});
- expect(findDropdown().exists()).toBe(true);
+ expect(findGlDropdown().exists()).toBe(true);
expect(findEditButton().exists()).toBe(false);
});
it('shows dropdown and delete item when user has update permission', () => {
mountComponent({ provide: { canUpdateTimelineEvent: true } });
- expect(findDropdown().exists()).toBe(true);
+ expect(findGlDropdown().exists()).toBe(true);
expect(findDeleteButton().exists()).toBe(true);
});
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 0b3ff0667b1..93cb7b5ae16 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
@@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
import eventHub from '~/issues/show/event_hub';
+jest.mock('~/issues/show/event_hub');
+
describe('TaskListItemActions component', () => {
let wrapper;
@@ -37,16 +39,12 @@ describe('TaskListItemActions component', () => {
});
it('emits event when `Convert to task` dropdown item is clicked', () => {
- jest.spyOn(eventHub, '$emit');
-
findConvertToTaskItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
});
it('emits event when `Delete` dropdown item is clicked', () => {
- jest.spyOn(eventHub, '$emit');
-
findDeleteItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 2980a6c33ee..561035242eb 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -19,7 +19,7 @@ const setupHTML = (initialData) => {
describe('Issue show index', () => {
describe('initIssueApp', () => {
- // https://gitlab.com/gitlab-org/gitlab/-/issues/390368
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/390368
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
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 0a887efee4b..f4f4936a134 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -137,7 +137,6 @@ describe('ProjectDropdown', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount });
- jest.clearAllMocks();
const mockSearchTerm = 'gitl';
await findDropdown().vm.$emit('search', mockSearchTerm);
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 a3bc8e861b2..cf2dacb50d8 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
@@ -104,7 +104,6 @@ describe('SourceBranchDropdown', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } });
await waitForPromises();
- jest.clearAllMocks();
const mockSearchTerm = 'mai';
await findListbox().vm.$emit('search', mockSearchTerm);
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 26a9d07321c..ea578836a12 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -7,6 +7,7 @@ import SignInPage from '~/jira_connect/subscriptions/pages/sign_in/sign_in_page.
import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions_page.vue';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import BrowserSupportAlert from '~/jira_connect/subscriptions/components/browser_support_alert.vue';
+import FeedbackBanner from '~/jira_connect/subscriptions/components/feedback_banner.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
@@ -31,6 +32,7 @@ describe('JiraConnectApp', () => {
const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
const findUserLink = () => wrapper.findComponent(UserLink);
const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
+ const findFeedbackBanner = () => wrapper.findComponent(FeedbackBanner);
const createComponent = ({ provide, initialState = {} } = {}) => {
store = createStore({ ...initialState, subscriptions: [mockSubscription] });
@@ -66,6 +68,12 @@ describe('JiraConnectApp', () => {
expect(findJiraConnectApp().exists()).toBe(false);
});
+ it('renders FeedbackBanner', () => {
+ createComponent();
+
+ expect(findFeedbackBanner().exists()).toBe(true);
+ });
+
describe.each`
scenario | currentUser | expectUserLink | expectSignInPage | expectSubscriptionsPage
${'user is not signed in'} | ${undefined} | ${false} | ${true} | ${false}
diff --git a/spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js b/spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js
new file mode 100644
index 00000000000..8debfaad5bb
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js
@@ -0,0 +1,45 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FeedbackBanner from '~/jira_connect/subscriptions/components/feedback_banner.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+
+describe('FeedbackBanner', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(FeedbackBanner);
+ };
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a banner with button', () => {
+ expect(findBanner().props()).toMatchObject({
+ title: FeedbackBanner.i18n.title,
+ buttonText: FeedbackBanner.i18n.buttonText,
+ buttonLink: FeedbackBanner.feedbackIssueUrl,
+ });
+ });
+
+ it('uses localStorage with default value as false', () => {
+ expect(findLocalStorageSync().props().value).toBe(false);
+ });
+
+ describe('when banner is dimsissed', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('close');
+ });
+
+ it('hides the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+
+ it('updates localStorage value to true', () => {
+ expect(findLocalStorageSync().props().value).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index 394fc8ad43c..c925131dd9c 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -9,7 +9,7 @@ import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
import ErasedBlock from '~/jobs/components/job/erased_block.vue';
import JobApp from '~/jobs/components/job/job_app.vue';
import JobLog from '~/jobs/components/log/log.vue';
-import JobLogTopBar from '~/jobs/components/job/job_log_controllers.vue';
+import JobLogTopBar from 'ee_else_ce/jobs/components/job/job_log_controllers.vue';
import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
import StuckBlock from '~/jobs/components/job/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
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 8121aa1172f..39782130d38 100644
--- a/spec/frontend/jobs/components/job/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job/job_container_item_spec.js
@@ -9,8 +9,8 @@ import job from '../../mock_data';
describe('JobContainerItem', () => {
let wrapper;
- const findCiIconComponent = () => wrapper.findComponent(CiIcon);
- const findGlIconComponent = () => wrapper.findComponent(GlIcon);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
function createComponent(jobData = {}, props = { isActive: false, retried: false }) {
wrapper = shallowMount(JobContainerItem, {
@@ -30,9 +30,7 @@ describe('JobContainerItem', () => {
});
it('displays a status icon', () => {
- const ciIcon = findCiIconComponent();
-
- expect(ciIcon.props('status')).toBe(job.status);
+ expect(findCiIcon().props('status')).toBe(job.status);
});
it('displays the job name', () => {
@@ -52,9 +50,7 @@ describe('JobContainerItem', () => {
});
it('displays an arrow sprite icon', () => {
- const icon = findGlIconComponent();
-
- expect(icon.props('name')).toBe('arrow-right');
+ expect(findGlIcon().props('name')).toBe('arrow-right');
});
});
@@ -64,9 +60,7 @@ describe('JobContainerItem', () => {
});
it('displays a retry icon', () => {
- const icon = findGlIconComponent();
-
- expect(icon.props('name')).toBe('retry');
+ expect(findGlIcon().props('name')).toBe('retry');
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index b4ec00ab766..444d4a96f9c 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1140,4 +1140,38 @@ describe('common_utils', () => {
expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
});
});
+
+ describe('isCurrentUser', () => {
+ describe('when user is not signed in', () => {
+ it('returns `false`', () => {
+ window.gon.current_user_id = null;
+
+ expect(commonUtils.isCurrentUser(1)).toBe(false);
+ });
+ });
+
+ describe('when current user id does not match the provided user id', () => {
+ it('returns `false`', () => {
+ window.gon.current_user_id = 2;
+
+ expect(commonUtils.isCurrentUser(1)).toBe(false);
+ });
+ });
+
+ describe('when current user id matches the provided user id', () => {
+ it('returns `true`', () => {
+ window.gon.current_user_id = 1;
+
+ expect(commonUtils.isCurrentUser(1)).toBe(true);
+ });
+ });
+
+ describe('when provided user id is a string and it matches current user id', () => {
+ it('returns `true`', () => {
+ window.gon.current_user_id = 1;
+
+ expect(commonUtils.isCurrentUser('1')).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index e7a6367eeac..65018fe1625 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -152,3 +152,18 @@ describe('formatUtcOffset', () => {
expect(utils.formatUtcOffset(offset)).toEqual(expected);
});
});
+
+describe('humanTimeframe', () => {
+ it.each`
+ startDate | dueDate | returnValue
+ ${'2021-1-1'} | ${'2021-2-28'} | ${'Jan 1 – Feb 28, 2021'}
+ ${'2021-1-1'} | ${'2022-2-28'} | ${'Jan 1, 2021 – Feb 28, 2022'}
+ ${'2021-1-1'} | ${null} | ${'Jan 1, 2021 – No due date'}
+ ${null} | ${'2021-2-28'} | ${'No start date – Feb 28, 2021'}
+ `(
+ 'returns string "$returnValue" when startDate is $startDate and dueDate is $dueDate',
+ ({ startDate, dueDate, returnValue }) => {
+ expect(utils.humanTimeframe(startDate, dueDate)).toBe(returnValue);
+ },
+ );
+});
diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js
index c14cba3a62b..a95b46d1440 100644
--- a/spec/frontend/lib/utils/downloader_spec.js
+++ b/spec/frontend/lib/utils/downloader_spec.js
@@ -8,10 +8,6 @@ describe('Downloader', () => {
jest.spyOn(document, 'createElement').mockImplementation(() => a);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
describe('when inline file content is provided', () => {
const fileData = 'inline content';
const fileName = 'test.csv';
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index 2f71b26b29a..b97f5bf3c51 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -1,7 +1,12 @@
import {
serializeForm,
serializeFormObject,
+ safeTrim,
isEmptyValue,
+ hasMinimumLength,
+ isParseableAsInteger,
+ isIntegerGreaterThan,
+ isEmail,
parseRailsFormFields,
} from '~/lib/utils/forms';
@@ -99,6 +104,22 @@ describe('lib/utils/forms', () => {
});
});
+ describe('safeTrim', () => {
+ it.each`
+ input | returnValue
+ ${''} | ${''}
+ ${[]} | ${[]}
+ ${null} | ${null}
+ ${undefined} | ${undefined}
+ ${' '} | ${''}
+ ${'hello '} | ${'hello'}
+ ${'hello'} | ${'hello'}
+ ${0} | ${0}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(safeTrim(input)).toEqual(returnValue);
+ });
+ });
+
describe('isEmptyValue', () => {
it.each`
input | returnValue
@@ -106,14 +127,102 @@ describe('lib/utils/forms', () => {
${[]} | ${true}
${null} | ${true}
${undefined} | ${true}
+ ${' '} | ${true}
${'hello'} | ${false}
- ${' '} | ${false}
${0} | ${false}
`('returns $returnValue for value $input', ({ input, returnValue }) => {
expect(isEmptyValue(input)).toBe(returnValue);
});
});
+ describe('hasMinimumLength', () => {
+ it.each`
+ input | minLength | returnValue
+ ${['o', 't']} | ${1} | ${true}
+ ${'hello'} | ${3} | ${true}
+ ${' '} | ${2} | ${false}
+ ${''} | ${0} | ${false}
+ ${''} | ${8} | ${false}
+ ${[]} | ${0} | ${false}
+ ${null} | ${8} | ${false}
+ ${undefined} | ${8} | ${false}
+ ${'hello'} | ${8} | ${false}
+ ${0} | ${8} | ${false}
+ ${4} | ${1} | ${false}
+ `(
+ 'returns $returnValue for value $input and minLength $minLength',
+ ({ input, minLength, returnValue }) => {
+ expect(hasMinimumLength(input, minLength)).toBe(returnValue);
+ },
+ );
+ });
+
+ describe('isPareseableInteger', () => {
+ it.each`
+ input | returnValue
+ ${'0'} | ${true}
+ ${'12'} | ${true}
+ ${''} | ${false}
+ ${[]} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${'12.4'} | ${false}
+ ${'12ef'} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isParseableAsInteger(input)).toBe(returnValue);
+ });
+ });
+
+ describe('isIntegerGreaterThan', () => {
+ it.each`
+ input | greaterThan | returnValue
+ ${25} | ${8} | ${true}
+ ${'25'} | ${8} | ${true}
+ ${'4'} | ${1} | ${true}
+ ${'4'} | ${8} | ${false}
+ ${'9.5'} | ${8} | ${false}
+ ${'9.5e'} | ${8} | ${false}
+ ${['o', 't']} | ${0} | ${false}
+ ${'hello'} | ${0} | ${false}
+ ${' '} | ${0} | ${false}
+ ${''} | ${0} | ${false}
+ ${''} | ${8} | ${false}
+ ${[]} | ${0} | ${false}
+ ${null} | ${0} | ${false}
+ ${undefined} | ${0} | ${false}
+ ${'hello'} | ${0} | ${false}
+ ${0} | ${0} | ${false}
+ `(
+ 'returns $returnValue for value $input and greaterThan $greaterThan',
+ ({ input, greaterThan, returnValue }) => {
+ expect(isIntegerGreaterThan(input, greaterThan)).toBe(returnValue);
+ },
+ );
+ });
+
+ describe('isEmail', () => {
+ it.each`
+ input | returnValue
+ ${'user-with_special-chars@example.com'} | ${true}
+ ${'user@subdomain.example.com'} | ${true}
+ ${'user@example.com'} | ${true}
+ ${'user@example.co'} | ${true}
+ ${'user@example.c'} | ${false}
+ ${'user@example'} | ${false}
+ ${''} | ${false}
+ ${[]} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${'12'} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isEmail(input)).toBe(returnValue);
+ });
+ });
+
describe('serializeFormObject', () => {
it('returns an serialized object', () => {
const form = {
diff --git a/spec/frontend/lib/utils/ref_validator_spec.js b/spec/frontend/lib/utils/ref_validator_spec.js
index 7185ebf0a24..97896d74dff 100644
--- a/spec/frontend/lib/utils/ref_validator_spec.js
+++ b/spec/frontend/lib/utils/ref_validator_spec.js
@@ -65,9 +65,6 @@ describe('~/lib/utils/ref_validator', () => {
['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);
@@ -75,5 +72,25 @@ describe('~/lib/utils/ref_validator', () => {
expect(result.validationErrors).toContain(validationMessage);
});
});
+
+ // NOTE: control characters cannot be used in test names because they cause test report XML parsing errors
+ describe.each([
+ [
+ 'control-character x7f',
+ 'control-character\x7f',
+ validationMessages.ControlCharactersValidationMessage,
+ ],
+ [
+ 'control-character x15',
+ '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/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index e3c89bfed53..efc8c9b4459 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -221,9 +221,11 @@ describe('MembersTable', () => {
'col-actions',
'gl-display-none!',
'gl-lg-display-table-cell!',
+ 'gl-vertical-align-middle!',
]);
expect(findTableCellByMemberId('Actions', members[1].id).classes()).toStrictEqual([
'col-actions',
+ 'gl-vertical-align-middle!',
]);
});
});
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 1285404fd9f..fa188f50d54 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -238,6 +238,16 @@ describe('RoleDropdown', () => {
it('does not call updateMemberRole', () => {
expect(actions.updateMemberRole).not.toHaveBeenCalled();
});
+
+ it('re-enables dropdown', async () => {
+ await waitForPromises();
+
+ expect(findListbox().props('disabled')).toBe(false);
+ });
+
+ it('resets selected dropdown item', () => {
+ expect(findListbox().props('selected')).toBe(member.validRoles.Owner);
+ });
});
});
});
diff --git a/spec/frontend/merge_requests/generated_content_spec.js b/spec/frontend/merge_requests/generated_content_spec.js
new file mode 100644
index 00000000000..f56a67ec466
--- /dev/null
+++ b/spec/frontend/merge_requests/generated_content_spec.js
@@ -0,0 +1,310 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+import { MergeRequestGeneratedContent } from '~/merge_requests/generated_content';
+
+function findWarningElement() {
+ return document.querySelector('.js-ai-description-warning');
+}
+
+function findCloseButton() {
+ return findWarningElement()?.querySelector('.js-close-btn');
+}
+
+function findApprovalButton() {
+ return findWarningElement()?.querySelector('.js-ai-override-description');
+}
+
+function findCancelButton() {
+ return findWarningElement()?.querySelector('.js-cancel-btn');
+}
+
+function clickButton(button) {
+ button.dispatchEvent(new Event('click'));
+}
+
+describe('MergeRequestGeneratedContent', () => {
+ const warningDOM = `
+
+<div class="js-ai-description-warning hidden">
+ <button class="js-close-btn">X</button>
+ <button class="js-ai-override-description">Do AI</button>
+ <button class="js-cancel-btn">Cancel</button>
+</div>
+
+`;
+
+ describe('class basics', () => {
+ let gen;
+
+ beforeEach(() => {
+ gen = new MergeRequestGeneratedContent();
+ });
+
+ it.each`
+ description | property
+ ${'with no editor'} | ${'hasEditor'}
+ ${'with no warning'} | ${'hasWarning'}
+ ${'unable to replace the content'} | ${'canReplaceContent'}
+ `('begins $description', ({ property }) => {
+ expect(gen[property]).toBe(false);
+ });
+ });
+
+ describe('the internal editor representation', () => {
+ let gen;
+
+ it('accepts an editor during construction', () => {
+ gen = new MergeRequestGeneratedContent({ editor: {} });
+
+ expect(gen.hasEditor).toBe(true);
+ });
+
+ it('allows adding an editor through a public API after construction', () => {
+ gen = new MergeRequestGeneratedContent();
+
+ expect(gen.hasEditor).toBe(false);
+
+ gen.setEditor({});
+
+ expect(gen.hasEditor).toBe(true);
+ });
+ });
+
+ describe('generated content', () => {
+ let gen;
+
+ beforeEach(() => {
+ gen = new MergeRequestGeneratedContent();
+ });
+
+ it('can be provided to the instance through a public API', () => {
+ expect(gen.generatedContent).toBe(null);
+
+ gen.setGeneratedContent('generated content');
+
+ expect(gen.generatedContent).toBe('generated content');
+ });
+
+ it('can be cleared from the instance through a public API', () => {
+ gen.setGeneratedContent('generated content');
+
+ expect(gen.generatedContent).toBe('generated content');
+
+ gen.clearGeneratedContent();
+
+ expect(gen.generatedContent).toBe(null);
+ });
+ });
+
+ describe('warning element', () => {
+ let gen;
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it.each`
+ presence | withFixture
+ ${'is'} | ${true}
+ ${'is not'} | ${false}
+ `('`.hasWarning` is $withFixture when the element $presence in the DOM', ({ withFixture }) => {
+ if (withFixture) {
+ setHTMLFixture(warningDOM);
+ }
+
+ gen = new MergeRequestGeneratedContent();
+
+ expect(gen.hasWarning).toBe(withFixture);
+ });
+ });
+
+ describe('special cases', () => {
+ it.each`
+ description | value | props
+ ${'there is no internal editor representation, and no generated content'} | ${false} | ${{}}
+ ${'there is an internal editor representation, but no generated content'} | ${false} | ${{ editor: {} }}
+ ${'there is no internal editor representation, but there is generated content'} | ${false} | ${{ content: 'generated content' }}
+ ${'there is an internal editor representation, and there is generated content'} | ${true} | ${{ editor: {}, content: 'generated content' }}
+ `('`.canReplaceContent` is $value when $description', ({ value, props }) => {
+ const gen = new MergeRequestGeneratedContent();
+
+ if (props.editor) {
+ gen.setEditor(props.editor);
+ }
+ if (props.content) {
+ gen.setGeneratedContent(props.content);
+ }
+
+ expect(gen.canReplaceContent).toBe(value);
+ });
+ });
+
+ describe('behaviors', () => {
+ describe('UI', () => {
+ describe('warning element', () => {
+ let gen;
+
+ beforeEach(() => {
+ setHTMLFixture(warningDOM);
+ gen = new MergeRequestGeneratedContent({ editor: {} });
+
+ gen.setGeneratedContent('generated content');
+ });
+
+ describe('#showWarning', () => {
+ it("shows the warning if it exists in the DOM and if it's possible to replace the description", () => {
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+ });
+
+ it("does nothing if the warning doesn't exist or if it's not possible to replace the description", () => {
+ gen.setEditor(null);
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+
+ gen.setEditor({});
+ gen.setGeneratedContent(null);
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+
+ resetHTMLFixture();
+ gen = new MergeRequestGeneratedContent({ editor: {} });
+ gen.setGeneratedContent('generated content');
+
+ expect(() => gen.showWarning()).not.toThrow();
+ expect(findWarningElement()).toBe(null);
+ });
+ });
+
+ describe('#hideWarning', () => {
+ it('hides the warning', () => {
+ findWarningElement().classList.remove('hidden');
+
+ gen.hideWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ });
+
+ it("does nothing if there's no warning element", () => {
+ resetHTMLFixture();
+ gen = new MergeRequestGeneratedContent();
+
+ expect(() => gen.hideWarning()).not.toThrow();
+ expect(findWarningElement()).toBe(null);
+ });
+ });
+ });
+ });
+
+ describe('content', () => {
+ const editor = {};
+ let gen;
+
+ beforeEach(() => {
+ editor.setValue = jest.fn();
+ gen = new MergeRequestGeneratedContent({ editor });
+ });
+
+ describe('#replaceDescription', () => {
+ it("sets the instance's generated content value to the internal representation of the editor", () => {
+ gen.setGeneratedContent('generated content');
+
+ gen.replaceDescription();
+
+ expect(editor.setValue).toHaveBeenCalledWith('generated content');
+ });
+
+ it("does nothing if there's no editor or no generated content", () => {
+ // Starts with editor, but no content
+ gen.replaceDescription();
+
+ expect(editor.setValue).not.toHaveBeenCalled();
+
+ gen.setGeneratedContent('generated content');
+ gen.setEditor(null);
+
+ gen.replaceDescription();
+
+ expect(editor.setValue).not.toHaveBeenCalled();
+ });
+
+ it("clears the generated content so the warning can't be re-shown with stale content", () => {
+ gen.setGeneratedContent('generated content');
+
+ gen.replaceDescription();
+
+ expect(editor.setValue).toHaveBeenCalledWith('generated content');
+ expect(gen.hasEditor).toBe(true);
+ expect(gen.canReplaceContent).toBe(false);
+ expect(gen.generatedContent).toBe(null);
+ });
+ });
+ });
+ });
+
+ describe('events', () => {
+ describe('UI clicks', () => {
+ const editor = {};
+ let gen;
+
+ beforeEach(() => {
+ setHTMLFixture(warningDOM);
+ editor.setValue = jest.fn();
+ gen = new MergeRequestGeneratedContent({ editor });
+
+ gen.setGeneratedContent('generated content');
+ });
+
+ describe('banner close button', () => {
+ it('hides the warning element', () => {
+ const close = findCloseButton();
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+
+ clickButton(close);
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ });
+ });
+
+ describe('banner approval button', () => {
+ it('sends the generated content to the editor, clears the internal generated content, and hides the warning', () => {
+ const approve = findApprovalButton();
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+ expect(gen.generatedContent).toBe('generated content');
+ expect(editor.setValue).not.toHaveBeenCalled();
+
+ clickButton(approve);
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ expect(gen.generatedContent).toBe(null);
+ expect(editor.setValue).toHaveBeenCalledWith('generated content');
+ });
+ });
+
+ describe('banner cancel button', () => {
+ it('hides the warning element', () => {
+ const cancel = findCancelButton();
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+
+ clickButton(cancel);
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
new file mode 100644
index 00000000000..d1715ccd8f1
--- /dev/null
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
@@ -0,0 +1,39 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
+import { TITLE_LABEL } from '~/ml/model_registry/routes/models/index/translations';
+import { mockModels } from './mock_data';
+
+let wrapper;
+const createWrapper = (models = mockModels) => {
+ wrapper = shallowMountExtended(MlModelsIndexApp, {
+ propsData: { models },
+ });
+};
+
+const findModelLink = (index) => wrapper.findAllComponents(GlLink).at(index);
+const modelLinkText = (index) => findModelLink(index).text();
+const modelLinkHref = (index) => findModelLink(index).attributes('href');
+const findTitle = () => wrapper.findByText(TITLE_LABEL);
+
+describe('MlModelsIndex', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ describe('header', () => {
+ it('displays the title', () => {
+ expect(findTitle().exists()).toBe(true);
+ });
+ });
+
+ describe('model list', () => {
+ it('displays the models', () => {
+ expect(modelLinkHref(0)).toBe(mockModels[0].path);
+ expect(modelLinkText(0)).toBe(`${mockModels[0].name} / ${mockModels[0].version}`);
+
+ expect(modelLinkHref(1)).toBe(mockModels[1].path);
+ expect(modelLinkText(1)).toBe(`${mockModels[1].name} / ${mockModels[1].version}`);
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
new file mode 100644
index 00000000000..b8a999abbbd
--- /dev/null
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
@@ -0,0 +1,12 @@
+export const mockModels = [
+ {
+ name: 'model_1',
+ version: '1.0',
+ path: 'path/to/model_1',
+ },
+ {
+ name: 'model_2',
+ version: '1.0',
+ path: 'path/to/model_2',
+ },
+];
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
deleted file mode 100644
index 3b4554700b4..00000000000
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ /dev/null
@@ -1,155 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Dashboard template matches the default snapshot 1`] = `
-<div
- class="prometheus-graphs"
- data-testid="prometheus-graphs"
- environmentstate="available"
- metricsdashboardbasepath="/monitoring/monitor-project/-/metrics?environment=1"
- metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
->
- <div>
- <gl-alert-stub
- class="mb-3"
- dismissible="true"
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- showicon="true"
- title="Feature deprecation"
- variant="warning"
- >
- <gl-sprintf-stub
- message="The metrics feature was deprecated in GitLab 14.7."
- />
-
- <gl-sprintf-stub
- message="For information on a possible replacement %{epicStart} learn more about Opstrace %{epicEnd}."
- />
- </gl-alert-stub>
- </div>
-
- <div
- class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
- >
- <div
- class="gl-mb-3 gl-mr-3 gl-display-flex gl-sm-display-block"
- >
- <dashboards-dropdown-stub
- class="flex-grow-1"
- defaultbranch="master"
- id="monitor-dashboards-dropdown"
- toggle-class="dropdown-menu-toggle"
- />
- </div>
-
- <span
- aria-hidden="true"
- class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
- />
-
- <div
- class="mb-2 pr-2 d-flex d-sm-block"
- >
- <gl-dropdown-stub
- category="primary"
- class="flex-grow-1"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- data-testid="environments-dropdown"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
- id="monitor-environments-dropdown"
- menu-class="monitor-environment-dropdown-menu"
- size="medium"
- text="production"
- toggleclass="dropdown-menu-toggle"
- variant="default"
- >
- <div
- class="d-flex flex-column overflow-hidden"
- >
- <gl-dropdown-section-header-stub>
- Environment
- </gl-dropdown-section-header-stub>
-
- <gl-search-box-by-type-stub
- clearbuttontitle="Clear"
- value=""
- />
-
- <div
- class="flex-fill overflow-auto"
- />
-
- <div
- class="text-secondary no-matches-message"
- >
-
- No matching results
-
- </div>
- </div>
- </gl-dropdown-stub>
- </div>
-
- <div
- class="mb-2 pr-2 d-flex d-sm-block"
- >
- <date-time-picker-stub
- class="flex-grow-1 show-last-dropdown"
- customenabled="true"
- options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
- value="[object Object]"
- />
- </div>
-
- <div
- class="mb-2 pr-2 d-flex d-sm-block"
- >
- <refresh-button-stub />
- </div>
-
- <div
- class="flex-grow-1"
- />
-
- <div
- class="d-sm-flex"
- >
- <!---->
-
- <!---->
-
- <div
- class="gl-mb-3 gl-mr-3 d-flex d-sm-block"
- >
- <actions-menu-stub
- custommetricspath="/monitoring/monitor-project/prometheus/metrics"
- defaultbranch="master"
- isootbdashboard="true"
- validatequerypath="/monitoring/monitor-project/prometheus/metrics/validate_query"
- />
- </div>
-
- <!---->
- </div>
- </div>
-
- <empty-state-stub
- clusterspath="/monitoring/monitor-project/-/clusters"
- documentationpath="/help/administration/monitoring/prometheus/index.md"
- emptygettingstartedsvgpath="/images/illustrations/monitoring/getting_started.svg"
- emptyloadingsvgpath="/images/illustrations/monitoring/loading.svg"
- emptynodatasmallsvgpath="/images/illustrations/chart-empty-state-small.svg"
- emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg"
- emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg"
- selectedstate="gettingStarted"
- settingspath="/monitoring/monitor-project/-/settings/integrations/prometheus/edit"
- />
-</div>
-`;
diff --git a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
deleted file mode 100644
index 4483c9fd39f..00000000000
--- a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
+++ /dev/null
@@ -1,55 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EmptyState shows gettingStarted state 1`] = `
-<div>
- <!---->
-
- <gl-empty-state-stub
- contentclass=""
- description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
- invertindarkmode="true"
- primarybuttonlink="/clustersPath"
- primarybuttontext="Install on clusters"
- secondarybuttonlink="/settingsPath"
- secondarybuttontext="Configure existing installation"
- svgpath="/path/to/getting-started.svg"
- title="Get started with performance monitoring"
- />
-</div>
-`;
-
-exports[`EmptyState shows noData state 1`] = `
-<div>
- <!---->
-
- <gl-empty-state-stub
- contentclass=""
- description="You are connected to the Prometheus server, but there is currently no data to display."
- invertindarkmode="true"
- primarybuttonlink="/settingsPath"
- primarybuttontext="Configure Prometheus"
- secondarybuttonlink=""
- secondarybuttontext=""
- svgpath="/path/to/no-data.svg"
- title="No data found"
- />
-</div>
-`;
-
-exports[`EmptyState shows unableToConnect state 1`] = `
-<div>
- <!---->
-
- <gl-empty-state-stub
- contentclass=""
- description="Ensure connectivity is available from the GitLab server to the Prometheus server"
- invertindarkmode="true"
- primarybuttonlink="/documentationPath"
- primarybuttontext="View documentation"
- secondarybuttonlink="/settingsPath"
- secondarybuttontext="Configure Prometheus"
- svgpath="/path/to/unable-to-connect.svg"
- title="Unable to connect to Prometheus server"
- />
-</div>
-`;
diff --git a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
deleted file mode 100644
index 42a16a39dfd..00000000000
--- a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
+++ /dev/null
@@ -1,160 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`GroupEmptyState given state BAD_QUERY passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": null,
- "invertInDarkMode": true,
- "primaryButtonLink": "/path/to/settings",
- "primaryButtonText": "Verify configuration",
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Query cannot be processed",
-}
-`;
-
-exports[`GroupEmptyState given state BAD_QUERY renders the slotted content 1`] = `
-<div>
- <div>
- The Prometheus server responded with "bad request". Please check your queries are correct and are supported in your Prometheus version.
- <a
- href="/path/to/docs"
- >
- More information
- </a>
- </div>
-</div>
-`;
-
-exports[`GroupEmptyState given state CONNECTION_FAILED passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating.",
- "invertInDarkMode": true,
- "primaryButtonLink": "/path/to/settings",
- "primaryButtonText": "Verify configuration",
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Connection failed",
-}
-`;
-
-exports[`GroupEmptyState given state CONNECTION_FAILED renders the slotted content 1`] = `<div />`;
-
-exports[`GroupEmptyState given state FOO STATE passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "An error occurred while loading the data. Please try again.",
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "An error has occurred",
-}
-`;
-
-exports[`GroupEmptyState given state FOO STATE renders the slotted content 1`] = `<div />`;
-
-exports[`GroupEmptyState given state LOADING passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.",
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Waiting for performance data",
-}
-`;
-
-exports[`GroupEmptyState given state LOADING renders the slotted content 1`] = `<div />`;
-
-exports[`GroupEmptyState given state NO_DATA passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": null,
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "No data to display",
-}
-`;
-
-exports[`GroupEmptyState given state NO_DATA renders the slotted content 1`] = `
-<div>
- <div>
- The data source is connected, but there is no data to display.
- <a
- href="/path/to/docs"
- >
- More information
- </a>
- </div>
-</div>
-`;
-
-exports[`GroupEmptyState given state TIMEOUT passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": null,
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Connection timed out",
-}
-`;
-
-exports[`GroupEmptyState given state TIMEOUT renders the slotted content 1`] = `
-<div>
- <div>
- Charts can't be displayed as the request for data has timed out.
- <a
- href="/path/to/docs"
- >
- More information
- </a>
- </div>
-</div>
-`;
-
-exports[`GroupEmptyState given state UNKNOWN_ERROR passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "An error occurred while loading the data. Please try again.",
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "An error has occurred",
-}
-`;
-
-exports[`GroupEmptyState given state UNKNOWN_ERROR renders the slotted content 1`] = `<div />`;
diff --git a/spec/frontend/monitoring/components/charts/annotations_spec.js b/spec/frontend/monitoring/components/charts/annotations_spec.js
deleted file mode 100644
index 1eac0935fe4..00000000000
--- a/spec/frontend/monitoring/components/charts/annotations_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { generateAnnotationsSeries } from '~/monitoring/components/charts/annotations';
-import { deploymentData, annotationsData } from '../../mock_data';
-
-describe('annotations spec', () => {
- describe('generateAnnotationsSeries', () => {
- it('with default options', () => {
- const annotations = generateAnnotationsSeries();
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: [],
- markLine: {
- data: [],
- symbol: 'none',
- silent: true,
- },
- }),
- );
- });
-
- it('when only deployments data is passed', () => {
- const annotations = generateAnnotationsSeries({ deployments: deploymentData });
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: expect.any(Array),
- markLine: {
- data: [],
- symbol: 'none',
- silent: true,
- },
- }),
- );
-
- annotations.data.forEach((annotation) => {
- expect(annotation).toEqual(expect.any(Object));
- });
-
- expect(annotations.data).toHaveLength(deploymentData.length);
- });
-
- it('when only annotations data is passed', () => {
- const annotations = generateAnnotationsSeries({
- annotations: annotationsData,
- });
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: expect.any(Array),
- markLine: expect.any(Object),
- markPoint: expect.any(Object),
- }),
- );
-
- annotations.markLine.data.forEach((annotation) => {
- expect(annotation).toEqual(expect.any(Object));
- });
-
- expect(annotations.data).toHaveLength(0);
- expect(annotations.markLine.data).toHaveLength(annotationsData.length);
- expect(annotations.markPoint.data).toHaveLength(annotationsData.length);
- });
-
- it('when deployments and annotations data is passed', () => {
- const annotations = generateAnnotationsSeries({
- deployments: deploymentData,
- annotations: annotationsData,
- });
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: expect.any(Array),
- markLine: expect.any(Object),
- markPoint: expect.any(Object),
- }),
- );
-
- annotations.markLine.data.forEach((annotation) => {
- expect(annotation).toEqual(expect.any(Object));
- });
-
- expect(annotations.data).toHaveLength(deploymentData.length);
- expect(annotations.markLine.data).toHaveLength(annotationsData.length);
- expect(annotations.markPoint.data).toHaveLength(annotationsData.length);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
deleted file mode 100644
index 3674a49f42c..00000000000
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ /dev/null
@@ -1,304 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import Anomaly from '~/monitoring/components/charts/anomaly.vue';
-
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import { colorValues } from '~/monitoring/constants';
-import { anomalyGraphData } from '../../graph_data';
-import { anomalyDeploymentData, mockProjectDir } from '../../mock_data';
-
-const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
-
-const TEST_UPPER = 11;
-const TEST_LOWER = 9;
-
-describe('Anomaly chart component', () => {
- let wrapper;
-
- const setupAnomalyChart = (props) => {
- wrapper = shallowMount(Anomaly, {
- propsData: { ...props },
- });
- };
- const findTimeSeries = () => wrapper.findComponent(MonitorTimeSeriesChart);
- const getTimeSeriesProps = () => findTimeSeries().props();
-
- describe('wrapped monitor-time-series-chart component', () => {
- const mockValues = ['10', '10', '10'];
-
- const mockGraphData = anomalyGraphData(
- {},
- {
- upper: mockValues.map(() => String(TEST_UPPER)),
- values: mockValues,
- lower: mockValues.map(() => String(TEST_LOWER)),
- },
- );
-
- const inputThresholds = ['some threshold'];
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: mockGraphData,
- deploymentData: anomalyDeploymentData,
- thresholds: inputThresholds,
- projectPath: mockProjectPath,
- });
- });
-
- it('renders correctly', () => {
- expect(findTimeSeries().exists()).toBe(true);
- });
-
- describe('receives props correctly', () => {
- describe('graph-data', () => {
- it('receives a single "metric" series', () => {
- const { graphData } = getTimeSeriesProps();
- expect(graphData.metrics.length).toBe(1);
- });
-
- it('receives "metric" with all data', () => {
- const { graphData } = getTimeSeriesProps();
- const metric = graphData.metrics[0];
- const expectedMetric = mockGraphData.metrics[0];
- expect(metric).toEqual(expectedMetric);
- });
-
- it('receives the "metric" results', () => {
- const { graphData } = getTimeSeriesProps();
- const { result } = graphData.metrics[0];
- const { values } = result[0];
-
- expect(values).toEqual([
- [expect.any(String), 10],
- [expect.any(String), 10],
- [expect.any(String), 10],
- ]);
- });
- });
-
- describe('option', () => {
- let option;
- let series;
-
- beforeEach(() => {
- ({ option } = getTimeSeriesProps());
- ({ series } = option);
- });
-
- it('contains a boundary band', () => {
- expect(series).toEqual(expect.any(Array));
- expect(series.length).toEqual(2); // 1 upper + 1 lower boundaries
- expect(series[0].stack).toEqual(series[1].stack);
-
- series.forEach((s) => {
- expect(s.type).toBe('line');
- expect(s.lineStyle.width).toBe(0);
- expect(s.lineStyle.color).toMatch(/rgba\(.+\)/);
- expect(s.lineStyle.color).toMatch(s.color);
- expect(s.symbol).toEqual('none');
- });
- });
-
- it('upper boundary values are stacked on top of lower boundary', () => {
- const [lowerSeries, upperSeries] = series;
-
- lowerSeries.data.forEach(([, y]) => {
- expect(y).toBeCloseTo(TEST_LOWER);
- });
-
- upperSeries.data.forEach(([, y]) => {
- expect(y).toBeCloseTo(TEST_UPPER - TEST_LOWER);
- });
- });
- });
-
- describe('series-config', () => {
- let seriesConfig;
-
- beforeEach(() => {
- ({ seriesConfig } = getTimeSeriesProps());
- });
-
- it('display symbols is enabled', () => {
- expect(seriesConfig).toEqual(
- expect.objectContaining({
- type: 'line',
- symbol: 'circle',
- showSymbol: true,
- symbolSize: expect.any(Function),
- itemStyle: {
- color: expect.any(Function),
- },
- }),
- );
- });
-
- it('does not display anomalies', () => {
- const { symbolSize, itemStyle } = seriesConfig;
- mockValues.forEach((v, dataIndex) => {
- const size = symbolSize(null, { dataIndex });
- const color = itemStyle.color({ dataIndex });
-
- // normal color and small size
- expect(size).toBeCloseTo(0);
- expect(color).toBe(colorValues.primaryColor);
- });
- });
-
- it('can format y values (to use in tooltips)', () => {
- mockValues.forEach((v, dataIndex) => {
- const formatted = wrapper.vm.yValueFormatted(0, dataIndex);
- expect(parseFloat(formatted)).toEqual(parseFloat(v));
- });
- });
- });
-
- describe('inherited properties', () => {
- it('"deployment-data" keeps the same value', () => {
- const { deploymentData } = getTimeSeriesProps();
- expect(deploymentData).toEqual(anomalyDeploymentData);
- });
- it('"projectPath" keeps the same value', () => {
- const { projectPath } = getTimeSeriesProps();
- expect(projectPath).toEqual(mockProjectPath);
- });
- });
- });
- });
-
- describe('with no boundary data', () => {
- const noBoundaryData = anomalyGraphData(
- {},
- {
- upper: [],
- values: ['10', '10', '10'],
- lower: [],
- },
- );
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: noBoundaryData,
- deploymentData: anomalyDeploymentData,
- });
- });
-
- describe('option', () => {
- let option;
- let series;
-
- beforeEach(() => {
- ({ option } = getTimeSeriesProps());
- ({ series } = option);
- });
-
- it('does not display a boundary band', () => {
- expect(series).toEqual(expect.any(Array));
- expect(series.length).toEqual(0); // no boundaries
- });
-
- it('can format y values (to use in tooltips)', () => {
- expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(10);
- expect(wrapper.vm.yValueFormatted(1, 0)).toBe(''); // missing boundary
- expect(wrapper.vm.yValueFormatted(2, 0)).toBe(''); // missing boundary
- });
- });
- });
-
- describe('with one anomaly', () => {
- const mockValues = ['10', '20', '10'];
-
- const oneAnomalyData = anomalyGraphData(
- {},
- {
- upper: mockValues.map(() => TEST_UPPER),
- values: mockValues,
- lower: mockValues.map(() => TEST_LOWER),
- },
- );
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: oneAnomalyData,
- deploymentData: anomalyDeploymentData,
- });
- });
-
- describe('series-config', () => {
- it('displays one anomaly', () => {
- const { seriesConfig } = getTimeSeriesProps();
- const { symbolSize, itemStyle } = seriesConfig;
-
- const bigDots = mockValues.filter((v, dataIndex) => {
- const size = symbolSize(null, { dataIndex });
- return size > 0.1;
- });
- const redDots = mockValues.filter((v, dataIndex) => {
- const color = itemStyle.color({ dataIndex });
- return color === colorValues.anomalySymbol;
- });
-
- expect(bigDots.length).toBe(1);
- expect(redDots.length).toBe(1);
- });
- });
- });
-
- describe('with offset', () => {
- const mockValues = ['10', '11', '12'];
- const mockUpper = ['20', '20', '20'];
- const mockLower = ['-1', '-2', '-3.70'];
- const expectedOffset = 4; // Lowest point in mock data is -3.70, it gets rounded
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: anomalyGraphData(
- {},
- {
- upper: mockUpper,
- values: mockValues,
- lower: mockLower,
- },
- ),
- deploymentData: anomalyDeploymentData,
- });
- });
-
- describe('receives props correctly', () => {
- describe('graph-data', () => {
- it('receives a single "metric" series', () => {
- const { graphData } = getTimeSeriesProps();
- expect(graphData.metrics.length).toBe(1);
- });
-
- it('receives "metric" results and applies the offset to them', () => {
- const { graphData } = getTimeSeriesProps();
- const { result } = graphData.metrics[0];
- const { values } = result[0];
-
- expect(values).toEqual(expect.any(Array));
-
- values.forEach(([, y], index) => {
- expect(y).toBeCloseTo(parseFloat(mockValues[index]) + expectedOffset);
- });
- });
- });
- });
-
- describe('option', () => {
- it('upper boundary values are stacked on top of lower boundary, plus the offset', () => {
- const { option } = getTimeSeriesProps();
- const { series } = option;
- const [lowerSeries, upperSeries] = series;
- lowerSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(parseFloat(mockLower[i]) + expectedOffset);
- });
-
- upperSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(parseFloat(mockUpper[i] - mockLower[i]));
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js
deleted file mode 100644
index 5339a7a525b..00000000000
--- a/spec/frontend/monitoring/components/charts/bar_spec.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { GlBarChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import Bar from '~/monitoring/components/charts/bar.vue';
-import { barGraphData } from '../../graph_data';
-
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
-}));
-
-describe('Bar component', () => {
- let barChart;
- let store;
- let graphData;
-
- beforeEach(() => {
- graphData = barGraphData();
-
- barChart = shallowMount(Bar, {
- propsData: {
- graphData,
- },
- store,
- });
- });
-
- afterEach(() => {
- barChart.destroy();
- });
-
- describe('wrapped components', () => {
- describe('GitLab UI bar chart', () => {
- let glbarChart;
- let chartData;
-
- beforeEach(() => {
- glbarChart = barChart.findComponent(GlBarChart);
- chartData = barChart.vm.chartData[graphData.metrics[0].label];
- });
-
- it('should display a label on the x axis', () => {
- expect(glbarChart.props('xAxisTitle')).toBe(graphData.xLabel);
- });
-
- it('should return chartData as array of arrays', () => {
- expect(chartData).toBeInstanceOf(Array);
-
- chartData.forEach((item) => {
- expect(item).toBeInstanceOf(Array);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
deleted file mode 100644
index cc38a3fd8a1..00000000000
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import ColumnChart from '~/monitoring/components/charts/column.vue';
-
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
-}));
-
-const yAxisName = 'Y-axis mock name';
-const yAxisFormat = 'bytes';
-const yAxisPrecistion = 3;
-const dataValues = [
- [1495700554.925, '8.0390625'],
- [1495700614.925, '8.0390625'],
- [1495700674.925, '8.0390625'],
-];
-
-describe('Column component', () => {
- let wrapper;
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMount(ColumnChart, {
- propsData: {
- graphData: {
- yAxis: {
- name: yAxisName,
- format: yAxisFormat,
- precision: yAxisPrecistion,
- },
- metrics: [
- {
- label: 'Mock data',
- result: [
- {
- metric: {},
- values: dataValues,
- },
- ],
- },
- ],
- },
- ...props,
- },
- });
- };
- const findChart = () => wrapper.findComponent(GlColumnChart);
- const chartProps = (prop) => findChart().props(prop);
-
- beforeEach(() => {
- createWrapper();
- });
-
- describe('xAxisLabel', () => {
- const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT
-
- const useXAxisFormatter = (date) => {
- const { xAxis } = chartProps('option');
- const { formatter } = xAxis.axisLabel;
- return formatter(date);
- };
-
- it('x-axis is formatted correctly in m/d h:MM TT format', () => {
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('by default, values are formatted in PT', () => {
- createWrapper();
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses local timezone, y-axis is formatted in PT', () => {
- createWrapper({ timezone: 'LOCAL' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses UTC, y-axis is formatted in UTC', () => {
- createWrapper({ timezone: 'UTC' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
- });
- });
-
- describe('wrapped components', () => {
- describe('GitLab UI column chart', () => {
- it('receives data properties needed for proper chart render', () => {
- expect(chartProps('bars')).toEqual([{ name: 'Mock data', data: dataValues }]);
- });
-
- it('passes the y axis name correctly', () => {
- expect(chartProps('yAxisTitle')).toBe(yAxisName);
- });
-
- it('passes the y axis configuration correctly', () => {
- expect(chartProps('option').yAxis).toMatchObject({
- name: yAxisName,
- axisLabel: {
- formatter: expect.any(Function),
- },
- scale: false,
- });
- });
-
- it('passes a dataZoom configuration', () => {
- expect(chartProps('option').dataZoom).toBeDefined();
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/empty_chart_spec.js b/spec/frontend/monitoring/components/charts/empty_chart_spec.js
deleted file mode 100644
index d755ed7c104..00000000000
--- a/spec/frontend/monitoring/components/charts/empty_chart_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-
-describe('Empty Chart component', () => {
- let emptyChart;
- const graphTitle = 'Memory Usage';
-
- beforeEach(() => {
- emptyChart = shallowMount(EmptyChart, {
- propsData: {
- graphTitle,
- },
- });
- });
-
- describe('Computed props', () => {
- it('sets the height for the svg container', () => {
- expect(emptyChart.vm.svgContainerStyle.height).toBe('300px');
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
deleted file mode 100644
index 33ea5e83598..00000000000
--- a/spec/frontend/monitoring/components/charts/gauge_spec.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import { GlGaugeChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import GaugeChart from '~/monitoring/components/charts/gauge.vue';
-import { gaugeChartGraphData } from '../../graph_data';
-
-describe('Gauge Chart component', () => {
- const defaultGraphData = gaugeChartGraphData();
-
- let wrapper;
-
- const findGaugeChart = () => wrapper.findComponent(GlGaugeChart);
-
- const createWrapper = ({ ...graphProps } = {}) => {
- wrapper = shallowMount(GaugeChart, {
- propsData: {
- graphData: {
- ...defaultGraphData,
- ...graphProps,
- },
- },
- });
- };
-
- describe('chart component', () => {
- it('is rendered when props are passed', () => {
- createWrapper();
-
- expect(findGaugeChart().exists()).toBe(true);
- });
- });
-
- describe('min and max', () => {
- const MIN_DEFAULT = 0;
- const MAX_DEFAULT = 100;
-
- it('are passed to chart component', () => {
- createWrapper();
-
- expect(findGaugeChart().props('min')).toBe(100);
- expect(findGaugeChart().props('max')).toBe(1000);
- });
-
- const invalidCases = [undefined, NaN, 'a string'];
-
- it.each(invalidCases)(
- 'if min has invalid value, defaults are used for both min and max',
- (invalidValue) => {
- createWrapper({ minValue: invalidValue });
-
- expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
- expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
- },
- );
-
- it.each(invalidCases)(
- 'if max has invalid value, defaults are used for both min and max',
- (invalidValue) => {
- createWrapper({ minValue: invalidValue });
-
- expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
- expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
- },
- );
-
- it('if min is bigger than max, defaults are used for both min and max', () => {
- createWrapper({ minValue: 100, maxValue: 0 });
-
- expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
- expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
- });
- });
-
- describe('thresholds', () => {
- it('thresholds are set on chart', () => {
- createWrapper();
-
- expect(findGaugeChart().props('thresholds')).toEqual([500, 800]);
- });
-
- it('when no thresholds are defined, a default threshold is defined at 95% of max_value', () => {
- createWrapper({
- minValue: 0,
- maxValue: 100,
- thresholds: {},
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([95]);
- });
-
- it('when out of min-max bounds thresholds are defined, a default threshold is defined at 95% of the range between min_value and max_value', () => {
- createWrapper({
- thresholds: {
- values: [-10, 1500],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
-
- describe('when mode is absolute', () => {
- it('only valid threshold values are used', () => {
- createWrapper({
- thresholds: {
- mode: 'absolute',
- values: [undefined, 10, 110, NaN, 'a string', 400],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([110, 400]);
- });
-
- it('if all threshold values are invalid, a default threshold is defined at 95% of the range between min_value and max_value', () => {
- createWrapper({
- thresholds: {
- mode: 'absolute',
- values: [NaN, undefined, 'a string', 1500],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
- });
-
- describe('when mode is percentage', () => {
- it('when values outside of 0-100 bounds are used, a default threshold is defined at 95% of max_value', () => {
- createWrapper({
- thresholds: {
- mode: 'percentage',
- values: [110],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
-
- it('if all threshold values are invalid, a default threshold is defined at 95% of max_value', () => {
- createWrapper({
- thresholds: {
- mode: 'percentage',
- values: [NaN, undefined, 'a string', 1500],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
- });
- });
-
- describe('split (the number of ticks on the chart arc)', () => {
- const SPLIT_DEFAULT = 10;
-
- it('is passed to chart as prop', () => {
- createWrapper();
-
- expect(findGaugeChart().props('splitNumber')).toBe(20);
- });
-
- it('if not explicitly set, passes a default value to chart', () => {
- createWrapper({ split: '' });
-
- expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
- });
-
- it('if set as a number that is not an integer, passes the default value to chart', () => {
- createWrapper({ split: 10.5 });
-
- expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
- });
-
- it('if set as a negative number, passes the default value to chart', () => {
- createWrapper({ split: -10 });
-
- expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
- });
- });
-
- describe('text (the text displayed on the gauge for the current value)', () => {
- it('displays the query result value when format is not set', () => {
- createWrapper({ format: '' });
-
- expect(findGaugeChart().props('text')).toBe('3');
- });
-
- it('displays the query result value when format is set to invalid value', () => {
- createWrapper({ format: 'invalid' });
-
- expect(findGaugeChart().props('text')).toBe('3');
- });
-
- it('displays a formatted query result value when format is set', () => {
- createWrapper();
-
- expect(findGaugeChart().props('text')).toBe('3kB');
- });
-
- it('displays a placeholder value when metric is empty', () => {
- createWrapper({ metrics: [] });
-
- expect(findGaugeChart().props('text')).toBe('--');
- });
- });
-
- describe('value', () => {
- it('correct value is passed', () => {
- createWrapper();
-
- expect(findGaugeChart().props('value')).toBe(3);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
deleted file mode 100644
index 54245cbdbc1..00000000000
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { GlHeatmap } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import Heatmap from '~/monitoring/components/charts/heatmap.vue';
-import { heatmapGraphData } from '../../graph_data';
-
-describe('Heatmap component', () => {
- let wrapper;
- let store;
-
- const findChart = () => wrapper.findComponent(GlHeatmap);
-
- const graphData = heatmapGraphData();
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMount(Heatmap, {
- propsData: {
- graphData: heatmapGraphData(),
- containerWidth: 100,
- ...props,
- },
- store,
- });
- };
-
- describe('wrapped chart', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should display a label on the x axis', () => {
- expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
- });
-
- it('should display a label on the y axis', () => {
- expect(wrapper.vm.yAxisName).toBe(graphData.y_label);
- });
-
- // According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data
- // each row of the heatmap chart is represented by an array inside another parent array
- // e.g. [[0, 0, 10]], the format represents the column, the row and finally the value
- // corresponding to the cell
-
- it('should return chartData with a length of x by y, with a length of 3 per array', () => {
- const row = wrapper.vm.chartData[0];
-
- expect(row.length).toBe(3);
- expect(wrapper.vm.chartData.length).toBe(6);
- });
-
- it('returns a series of labels for the x axis', () => {
- const { xAxisLabels } = wrapper.vm;
-
- expect(xAxisLabels.length).toBe(2);
- });
-
- describe('y axis labels', () => {
- const gmtLabels = ['8:10 PM', '8:12 PM', '8:14 PM'];
-
- it('y-axis labels are formatted in AM/PM format', () => {
- expect(findChart().props('yAxisLabels')).toEqual(gmtLabels);
- });
-
- describe('when in PT timezone', () => {
- const ptLabels = ['1:10 PM', '1:12 PM', '1:14 PM'];
- const utcLabels = gmtLabels; // Identical in this case
-
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('by default, y-axis is formatted in PT', () => {
- createWrapper();
- expect(findChart().props('yAxisLabels')).toEqual(ptLabels);
- });
-
- it('when the chart uses local timezone, y-axis is formatted in PT', () => {
- createWrapper({ timezone: 'LOCAL' });
- expect(findChart().props('yAxisLabels')).toEqual(ptLabels);
- });
-
- it('when the chart uses UTC, y-axis is formatted in UTC', () => {
- createWrapper({ timezone: 'UTC' });
- expect(findChart().props('yAxisLabels')).toEqual(utcLabels);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js
deleted file mode 100644
index 064ce6f204c..00000000000
--- a/spec/frontend/monitoring/components/charts/options_spec.js
+++ /dev/null
@@ -1,327 +0,0 @@
-import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
-import {
- getYAxisOptions,
- getTooltipFormatter,
- getValidThresholds,
-} from '~/monitoring/components/charts/options';
-
-describe('options spec', () => {
- describe('getYAxisOptions', () => {
- it('default options', () => {
- const options = getYAxisOptions();
-
- expect(options).toMatchObject({
- name: expect.any(String),
- axisLabel: {
- formatter: expect.any(Function),
- },
- scale: true,
- boundaryGap: [expect.any(Number), expect.any(Number)],
- });
-
- expect(options.name).not.toHaveLength(0);
- });
-
- it('name options', () => {
- const yAxisName = 'My axis values';
- const options = getYAxisOptions({
- name: yAxisName,
- });
-
- expect(options).toMatchObject({
- name: yAxisName,
- nameLocation: 'center',
- nameGap: expect.any(Number),
- });
- });
-
- it('formatter options defaults to engineering notation', () => {
- const options = getYAxisOptions();
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(3002.1)).toBe('3k');
- });
-
- it('formatter options allows for precision to be set explicitly', () => {
- const options = getYAxisOptions({
- precision: 4,
- });
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(5002.1)).toBe('5.0021k');
- });
-
- it('formatter options allows for overrides in milliseconds', () => {
- const options = getYAxisOptions({
- format: SUPPORTED_FORMATS.milliseconds,
- });
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(1.1234)).toBe('1.12ms');
- });
-
- it('formatter options allows for overrides in bytes', () => {
- const options = getYAxisOptions({
- format: SUPPORTED_FORMATS.bytes,
- });
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(1)).toBe('1.00B');
- });
- });
-
- describe('getTooltipFormatter', () => {
- it('default format', () => {
- const formatter = getTooltipFormatter();
-
- expect(formatter).toEqual(expect.any(Function));
- expect(formatter(0.11111)).toBe('111.1m');
- });
-
- it('defined format', () => {
- const formatter = getTooltipFormatter({
- format: SUPPORTED_FORMATS.bytes,
- });
-
- expect(formatter(1)).toBe('1.000B');
- });
- });
-
- describe('getValidThresholds', () => {
- const invalidCases = [null, undefined, NaN, 'a string', true, false];
-
- let thresholds;
-
- afterEach(() => {
- thresholds = null;
- });
-
- it('returns same thresholds when passed values within range', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([10, 50]);
- });
-
- it('filters out thresholds that are out of range', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [-5, 10, 110],
- });
-
- expect(thresholds).toEqual([10]);
- });
- it('filters out duplicate thresholds', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [5, 5, 10, 10],
- });
-
- expect(thresholds).toEqual([5, 10]);
- });
-
- it('sorts passed thresholds and applies only the first two in ascending order', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, 1, 35, 20, 5],
- });
-
- expect(thresholds).toEqual([1, 5]);
- });
-
- it('thresholds equal to min or max are filtered out', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [0, 100],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it.each(invalidCases)('invalid values for thresholds are filtered out', (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, invalidValue],
- });
-
- expect(thresholds).toEqual([10]);
- });
-
- describe('range', () => {
- it('when range is not defined, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('when min is not defined, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { max: 100 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('when max is not defined, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('when min is larger than max, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 100, max: 0 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it.each(invalidCases)(
- 'when min has invalid value, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: invalidValue, max: 100 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
-
- it.each(invalidCases)(
- 'when max has invalid value, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: invalidValue },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
- });
-
- describe('values', () => {
- it('if values parameter is omitted, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('if there are no values passed, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it.each(invalidCases)(
- 'if invalid values are passed, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [invalidValue],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
- });
-
- describe('mode', () => {
- it.each(invalidCases)(
- 'if invalid values are passed, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: invalidValue,
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
-
- it('if mode is not passed, empty result is returned', () => {
- thresholds = getValidThresholds({
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- describe('absolute mode', () => {
- it('absolute mode behaves correctly', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([10, 50]);
- });
- });
-
- describe('percentage mode', () => {
- it('percentage mode behaves correctly', () => {
- thresholds = getValidThresholds({
- mode: 'percentage',
- range: { min: 0, max: 1000 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([100, 500]);
- });
-
- const outOfPercentBoundsValues = [-1, 0, 100, 101];
- it.each(outOfPercentBoundsValues)(
- 'when values out of 0-100 range are passed, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'percentage',
- range: { min: 0, max: 1000 },
- values: [invalidValue],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
- });
- });
-
- it('calling without passing object parameter returns empty array', () => {
- thresholds = getValidThresholds();
-
- expect(thresholds).toEqual([]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
deleted file mode 100644
index fa31b479296..00000000000
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import { singleStatGraphData } from '../../graph_data';
-
-describe('Single Stat Chart component', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(SingleStatChart, {
- propsData: {
- graphData: singleStatGraphData({}, { unit: 'MB' }),
- ...props,
- },
- });
- };
-
- const findChart = () => wrapper.findComponent(GlSingleStat);
-
- beforeEach(() => {
- createComponent();
- });
-
- describe('computed', () => {
- describe('statValue', () => {
- it('should display the correct value', () => {
- expect(findChart().props('value')).toBe('1.00');
- });
-
- it('should display the correct value unit', () => {
- expect(findChart().props('unit')).toBe('MB');
- });
-
- it('should change the value representation to a percentile one', () => {
- createComponent({
- graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
- });
-
- expect(findChart().props('value')).toBe('75.83');
- expect(findChart().props('unit')).toBe('%');
- });
-
- it('should display NaN for non numeric maxValue values', () => {
- createComponent({
- graphData: singleStatGraphData({ max_value: 'not a number' }),
- });
-
- expect(findChart().props('value')).toContain('NaN');
- });
-
- it('should display NaN for missing query values', () => {
- createComponent({
- graphData: singleStatGraphData({ max_value: 120 }, { value: 'NaN' }),
- });
-
- expect(findChart().props('value')).toContain('NaN');
- });
-
- it('should not display `unit` when `unit` is undefined', () => {
- createComponent({
- graphData: singleStatGraphData({}, { unit: undefined }),
- });
-
- expect(findChart().props('value')).not.toContain('undefined');
- });
-
- it('should not display `unit` when `unit` is null', () => {
- createComponent({
- graphData: singleStatGraphData({}, { unit: null }),
- });
-
- expect(findChart().props('value')).not.toContain('null');
- });
-
- describe('when a field attribute is set', () => {
- it('displays a label value instead of metric value when field attribute is used', () => {
- createComponent({
- graphData: singleStatGraphData({ field: 'job' }, { isVector: true }),
- });
-
- expect(findChart().props('value')).toContain('prometheus');
- });
-
- it('displays No data to display if field attribute is not present', () => {
- createComponent({
- graphData: singleStatGraphData({ field: 'this-does-not-exist' }),
- });
-
- expect(findChart().props('value')).toContain('No data to display');
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
deleted file mode 100644
index 779ded090c2..00000000000
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ /dev/null
@@ -1,193 +0,0 @@
-import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import { cloneDeep } from 'lodash';
-import timezoneMock from 'timezone-mock';
-import { nextTick } from 'vue';
-import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-import { stackedColumnGraphData } from '../../graph_data';
-
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockImplementation((icon) => Promise.resolve(`${icon}-content`)),
-}));
-
-describe('Stacked column chart component', () => {
- const stackedColumnMockedData = stackedColumnGraphData();
-
- let wrapper;
-
- const findChart = () => wrapper.findComponent(GlStackedColumnChart);
- const findLegend = () => wrapper.findComponent(GlChartLegend);
-
- const createWrapper = (props = {}, mountingMethod = shallowMount) =>
- mountingMethod(StackedColumnChart, {
- propsData: {
- graphData: stackedColumnMockedData,
- ...props,
- },
- stubs: {
- GlPopover: true,
- },
- attachTo: document.body,
- });
-
- beforeEach(() => {
- wrapper = createWrapper({}, mount);
- });
-
- describe('when graphData is present', () => {
- beforeEach(async () => {
- createWrapper();
- await nextTick();
- });
-
- it('chart is rendered', () => {
- expect(findChart().exists()).toBe(true);
- });
-
- it('data should match the graphData y value for each series', () => {
- const data = findChart().props('bars');
-
- data.forEach((series, index) => {
- const { values } = stackedColumnMockedData.metrics[index].result[0];
- expect(series.data).toEqual(values.map((value) => value[1]));
- });
- });
-
- it('data should be the same length as the graphData metrics labels', () => {
- const barDataProp = findChart().props('bars');
-
- expect(barDataProp).toHaveLength(stackedColumnMockedData.metrics.length);
- barDataProp.forEach(({ name }, index) => {
- expect(stackedColumnMockedData.metrics[index].label).toBe(name);
- });
- });
-
- it('group by should be the same as the graphData first metric results', () => {
- const groupBy = findChart().props('groupBy');
-
- expect(groupBy).toEqual([
- '2015-07-01T20:10:50.000Z',
- '2015-07-01T20:12:50.000Z',
- '2015-07-01T20:14:50.000Z',
- ]);
- });
-
- it('chart options should configure data zoom and axis label', () => {
- const chartOptions = findChart().props('option');
- const xAxisType = findChart().props('xAxisType');
-
- expect(chartOptions).toMatchObject({
- dataZoom: [{ handleIcon: 'path://scroll-handle-content' }],
- xAxis: {
- axisLabel: { formatter: expect.any(Function) },
- },
- });
-
- expect(xAxisType).toBe('category');
- });
-
- it('chart options should configure category as x axis type', () => {
- const chartOptions = findChart().props('option');
- const xAxisType = findChart().props('xAxisType');
-
- expect(chartOptions).toMatchObject({
- xAxis: {
- type: 'category',
- },
- });
- expect(xAxisType).toBe('category');
- });
-
- it('format date is correct', () => {
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('12:01 PM');
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('date is shown in local time', () => {
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('4:01 AM');
- });
-
- it('date is shown in UTC', async () => {
- wrapper.setProps({ timezone: 'UTC' });
-
- await nextTick();
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('12:01 PM');
- });
- });
- });
-
- describe('when graphData has results missing', () => {
- beforeEach(async () => {
- const graphData = cloneDeep(stackedColumnMockedData);
-
- graphData.metrics[0].result = null;
-
- createWrapper({ graphData });
- await nextTick();
- });
-
- it('chart is rendered', () => {
- expect(findChart().exists()).toBe(true);
- });
- });
-
- describe('legend', () => {
- beforeEach(() => {
- wrapper = createWrapper({}, mount);
- });
-
- it('allows user to override legend label texts using props', async () => {
- const legendRelatedProps = {
- legendMinText: 'legendMinText',
- legendMaxText: 'legendMaxText',
- legendAverageText: 'legendAverageText',
- legendCurrentText: 'legendCurrentText',
- };
- wrapper.setProps({
- ...legendRelatedProps,
- });
-
- await nextTick();
- expect(findChart().props()).toMatchObject(legendRelatedProps);
- });
-
- it('should render a tabular legend layout by default', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
-
- describe('when inline legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'inline',
- });
- });
-
- it('should render an inline legend layout', () => {
- expect(findLegend().props('layout')).toBe('inline');
- });
- });
-
- describe('when table legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'table',
- });
- });
-
- it('should render a tabular legend layout', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
deleted file mode 100644
index c1b51f71a7e..00000000000
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ /dev/null
@@ -1,748 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import {
- GlAreaChart,
- GlLineChart,
- GlChartSeriesLabel,
- GlChartLegend,
-} from '@gitlab/ui/dist/charts';
-import { mount, shallowMount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
-import TimeSeries from '~/monitoring/components/charts/time_series.vue';
-import { panelTypes, chartHeight } from '~/monitoring/constants';
-import { timeSeriesGraphData } from '../../graph_data';
-import {
- deploymentData,
- mockProjectDir,
- annotationsData,
- mockFixedTimeRange,
-} from '../../mock_data';
-
-jest.mock('lodash/throttle', () =>
- // this throttle mock executes immediately
- jest.fn((func) => {
- // eslint-disable-next-line no-param-reassign
- func.cancel = jest.fn();
- return func;
- }),
-);
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockImplementation((icon) => Promise.resolve(`${icon}-content`)),
-}));
-
-describe('Time series component', () => {
- const defaultGraphData = timeSeriesGraphData();
- let wrapper;
-
- const createWrapper = (
- { graphData = defaultGraphData, ...props } = {},
- mountingMethod = shallowMount,
- ) => {
- wrapper = mountingMethod(TimeSeries, {
- propsData: {
- graphData,
- deploymentData,
- annotations: annotationsData,
- projectPath: `${TEST_HOST}${mockProjectDir}`,
- timeRange: mockFixedTimeRange,
- ...props,
- },
- stubs: {
- GlPopover: true,
- GlLineChart,
- GlAreaChart,
- },
- attachTo: document.body,
- });
- };
-
- describe('With a single time series', () => {
- describe('general functions', () => {
- const findChart = () => wrapper.findComponent({ ref: 'chart' });
-
- beforeEach(async () => {
- createWrapper({}, mount);
- await nextTick();
- });
-
- it('allows user to override legend label texts using props', async () => {
- const legendRelatedProps = {
- legendMinText: 'legendMinText',
- legendMaxText: 'legendMaxText',
- legendAverageText: 'legendAverageText',
- legendCurrentText: 'legendCurrentText',
- };
- wrapper.setProps({
- ...legendRelatedProps,
- });
-
- await nextTick();
- expect(findChart().props()).toMatchObject(legendRelatedProps);
- });
-
- it('chart sets a default height', () => {
- createWrapper();
- expect(wrapper.props('height')).toBe(chartHeight);
- });
-
- it('chart has a configurable height', async () => {
- const mockHeight = 599;
- createWrapper();
-
- wrapper.setProps({ height: mockHeight });
- await nextTick();
- expect(wrapper.props('height')).toBe(mockHeight);
- });
-
- describe('events', () => {
- describe('datazoom', () => {
- let eChartMock;
- let startValue;
- let endValue;
-
- beforeEach(async () => {
- eChartMock = {
- handlers: {},
- getOption: () => ({
- dataZoom: [
- {
- startValue,
- endValue,
- },
- ],
- }),
- off: jest.fn((eChartEvent) => {
- delete eChartMock.handlers[eChartEvent];
- }),
- on: jest.fn((eChartEvent, fn) => {
- eChartMock.handlers[eChartEvent] = fn;
- }),
- };
-
- createWrapper({}, mount);
- await nextTick();
- findChart().vm.$emit('created', eChartMock);
- });
-
- it('handles datazoom event from chart', () => {
- startValue = 1577836800000; // 2020-01-01T00:00:00.000Z
- endValue = 1577840400000; // 2020-01-01T01:00:00.000Z
- eChartMock.handlers.datazoom();
-
- expect(wrapper.emitted('datazoom')).toHaveLength(1);
- expect(wrapper.emitted('datazoom')[0]).toEqual([
- {
- start: new Date(startValue).toISOString(),
- end: new Date(endValue).toISOString(),
- },
- ]);
- });
- });
- });
-
- describe('methods', () => {
- describe('formatTooltipText', () => {
- const mockCommitUrl = deploymentData[0].commitUrl;
- const mockDate = deploymentData[0].created_at;
- const mockSha = 'f5bcd1d9';
- const mockLineSeriesData = () => ({
- seriesData: [
- {
- seriesName: wrapper.vm.chartData[0].name,
- componentSubType: 'line',
- value: [mockDate, 5.55555],
- dataIndex: 0,
- },
- ],
- value: mockDate,
- });
-
- const annotationsMetadata = {
- tooltipData: {
- sha: mockSha,
- commitUrl: mockCommitUrl,
- },
- };
-
- const mockAnnotationsSeriesData = {
- seriesData: [
- {
- componentSubType: 'scatter',
- seriesName: 'series01',
- dataIndex: 0,
- value: [mockDate, 5.55555],
- type: 'scatter',
- name: 'deployments',
- },
- ],
- value: mockDate,
- };
-
- it('does not throw error if data point is outside the zoom range', () => {
- const seriesDataWithoutValue = {
- ...mockLineSeriesData(),
- seriesData: mockLineSeriesData().seriesData.map((data) => ({
- ...data,
- value: undefined,
- })),
- };
- expect(wrapper.vm.formatTooltipText(seriesDataWithoutValue)).toBeUndefined();
- });
-
- describe('when series is of line type', () => {
- beforeEach(async () => {
- createWrapper({}, mount);
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- });
-
- it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
-
- it('formats tooltip content', () => {
- const name = 'Metric 1';
- const value = '5.556';
- const dataIndex = 0;
- const seriesLabel = wrapper.findComponent(GlChartSeriesLabel);
-
- expect(seriesLabel.vm.color).toBe('');
-
- expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
- expect(wrapper.vm.tooltip.content).toEqual([
- { name, value, dataIndex, color: undefined },
- ]);
-
- expect(
- shallowWrapperContainsSlotText(
- wrapper.findComponent(GlLineChart),
- 'tooltip-content',
- value,
- ),
- ).toBe(true);
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- // Note: node.js env renders (GMT-0700), in the browser we see (PDT)
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('formats tooltip title in local timezone by default', async () => {
- createWrapper();
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
- });
-
- it('formats tooltip title in local timezone', async () => {
- createWrapper({ timezone: 'LOCAL' });
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
- });
-
- it('formats tooltip title in UTC format', async () => {
- createWrapper({ timezone: 'UTC' });
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
- });
- });
-
- describe('when series is of scatter type, for deployments', () => {
- beforeEach(async () => {
- wrapper.vm.formatTooltipText({
- ...mockAnnotationsSeriesData,
- seriesData: mockAnnotationsSeriesData.seriesData.map((data) => ({
- ...data,
- data: annotationsMetadata,
- })),
- });
- await nextTick();
- });
-
- it('set tooltip type to deployments', () => {
- expect(wrapper.vm.tooltip.type).toBe('deployments');
- });
-
- it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
-
- it('formats tooltip sha', () => {
- expect(wrapper.vm.tooltip.sha).toBe('f5bcd1d9');
- });
-
- it('formats tooltip commit url', () => {
- expect(wrapper.vm.tooltip.commitUrl).toBe(mockCommitUrl);
- });
- });
-
- describe('when series is of scatter type and deployments data is missing', () => {
- beforeEach(async () => {
- wrapper.vm.formatTooltipText(mockAnnotationsSeriesData);
- await nextTick();
- });
-
- it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
-
- it('formats tooltip sha', () => {
- expect(wrapper.vm.tooltip.sha).toBeUndefined();
- });
-
- it('formats tooltip commit url', () => {
- expect(wrapper.vm.tooltip.commitUrl).toBeUndefined();
- });
- });
- });
-
- describe('formatAnnotationsTooltipText', () => {
- const annotationsMetadata = {
- name: 'annotations',
- xAxis: annotationsData[0].from,
- yAxis: 0,
- tooltipData: {
- title: '2020/02/19 10:01:41',
- content: annotationsData[0].description,
- },
- };
-
- const mockMarkPoint = {
- componentType: 'markPoint',
- name: 'annotations',
- value: undefined,
- data: annotationsMetadata,
- };
-
- it('formats tooltip title and sets tooltip content', () => {
- const formattedTooltipData = wrapper.vm.formatAnnotationsTooltipText(mockMarkPoint);
- expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM (UTC)');
- expect(formattedTooltipData.content).toBe(annotationsMetadata.tooltipData.content);
- });
- });
- });
-
- describe('computed', () => {
- const getChartOptions = () => findChart().props('option');
-
- describe('chartData', () => {
- let chartData;
- const seriesData = () => chartData[0];
-
- beforeEach(() => {
- ({ chartData } = wrapper.vm);
- });
-
- it('utilizes all data points', () => {
- expect(chartData.length).toBe(1);
- expect(seriesData().data.length).toBe(3);
- });
-
- it('creates valid data', () => {
- const { data } = seriesData();
-
- expect(
- data.filter(
- ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
- ).length,
- ).toBe(data.length);
- });
-
- it('formats line width correctly', () => {
- expect(chartData[0].lineStyle.width).toBe(2);
- });
- });
-
- describe('chartOptions', () => {
- describe('x-Axis bounds', () => {
- it('is set to the time range bounds', () => {
- expect(getChartOptions().xAxis).toMatchObject({
- min: mockFixedTimeRange.start,
- max: mockFixedTimeRange.end,
- });
- });
-
- it('is not set if time range is not set or incorrectly set', async () => {
- wrapper.setProps({
- timeRange: {},
- });
- await nextTick();
- expect(getChartOptions().xAxis).not.toHaveProperty('min');
- expect(getChartOptions().xAxis).not.toHaveProperty('max');
- });
- });
-
- describe('dataZoom', () => {
- it('renders with scroll handle icons', () => {
- expect(getChartOptions().dataZoom).toHaveLength(1);
- expect(getChartOptions().dataZoom[0]).toMatchObject({
- handleIcon: 'path://scroll-handle-content',
- });
- });
- });
-
- describe('xAxis pointer', () => {
- it('snap is set to false by default', () => {
- expect(getChartOptions().xAxis.axisPointer.snap).toBe(false);
- });
- });
-
- describe('are extended by `option`', () => {
- const mockSeriesName = 'Extra series 1';
- const mockOption = {
- option1: 'option1',
- option2: 'option2',
- };
-
- it('arbitrary options', async () => {
- wrapper.setProps({
- option: mockOption,
- });
-
- await nextTick();
- expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
- });
-
- it('additional series', async () => {
- wrapper.setProps({
- option: {
- series: [
- {
- name: mockSeriesName,
- type: 'line',
- data: [],
- },
- ],
- },
- });
-
- await nextTick();
- const optionSeries = getChartOptions().series;
-
- expect(optionSeries.length).toEqual(2);
- expect(optionSeries[0].name).toEqual(mockSeriesName);
- });
-
- it('additional y-axis data', async () => {
- const mockCustomYAxisOption = {
- name: 'Custom y-axis label',
- axisLabel: {
- formatter: jest.fn(),
- },
- };
-
- wrapper.setProps({
- option: {
- yAxis: mockCustomYAxisOption,
- },
- });
-
- await nextTick();
- const { yAxis } = getChartOptions();
-
- expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
- });
-
- it('additional x axis data', async () => {
- const mockCustomXAxisOption = {
- name: 'Custom x axis label',
- };
-
- wrapper.setProps({
- option: {
- xAxis: mockCustomXAxisOption,
- },
- });
-
- await nextTick();
- const { xAxis } = getChartOptions();
-
- expect(xAxis).toMatchObject(mockCustomXAxisOption);
- });
- });
-
- describe('yAxis formatter', () => {
- let dataFormatter;
- let deploymentFormatter;
-
- beforeEach(() => {
- dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter;
- deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
- });
-
- it('formats by default to precision notation', () => {
- expect(dataFormatter(0.88888)).toBe('889m');
- });
-
- it('deployment formatter is set as is required to display a tooltip', () => {
- expect(deploymentFormatter).toEqual(expect.any(Function));
- });
- });
- });
-
- describe('annotationSeries', () => {
- it('utilizes deployment data', () => {
- const annotationSeries = wrapper.vm.chartOptionSeries[0];
- expect(annotationSeries.yAxisIndex).toBe(1); // same as annotations y axis
- expect(annotationSeries.data).toEqual([
- expect.objectContaining({
- symbolSize: 14,
- symbol: 'path://rocket-content',
- value: ['2019-07-16T10:14:25.589Z', expect.any(Number)],
- }),
- expect.objectContaining({
- symbolSize: 14,
- symbol: 'path://rocket-content',
- value: ['2019-07-16T11:14:25.589Z', expect.any(Number)],
- }),
- expect.objectContaining({
- symbolSize: 14,
- symbol: 'path://rocket-content',
- value: ['2019-07-16T12:14:25.589Z', expect.any(Number)],
- }),
- ]);
- });
- });
-
- describe('xAxisLabel', () => {
- const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT
-
- const useXAxisFormatter = (date) => {
- const { xAxis } = getChartOptions();
- const { formatter } = xAxis.axisLabel;
- return formatter(date);
- };
-
- it('x-axis is formatted correctly in m/d h:MM TT format', () => {
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('by default, values are formatted in PT', () => {
- createWrapper();
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses local timezone, y-axis is formatted in PT', () => {
- createWrapper({ timezone: 'LOCAL' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses UTC, y-axis is formatted in UTC', () => {
- createWrapper({ timezone: 'UTC' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
- });
- });
-
- describe('yAxisLabel', () => {
- it('y-axis is configured correctly', () => {
- const { yAxis } = getChartOptions();
-
- expect(yAxis).toHaveLength(2);
-
- const [dataAxis, deploymentAxis] = yAxis;
-
- expect(dataAxis.boundaryGap).toHaveLength(2);
- expect(dataAxis.scale).toBe(true);
-
- expect(deploymentAxis.show).toBe(false);
- expect(deploymentAxis.min).toEqual(expect.any(Number));
- expect(deploymentAxis.max).toEqual(expect.any(Number));
- expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max);
- });
-
- it('constructs a label for the chart y-axis', () => {
- const { yAxis } = getChartOptions();
-
- expect(yAxis[0].name).toBe('Y Axis');
- });
- });
- });
- });
-
- describe('wrapped components', () => {
- const glChartComponents = [
- {
- chartType: panelTypes.AREA_CHART,
- component: GlAreaChart,
- },
- {
- chartType: panelTypes.LINE_CHART,
- component: GlLineChart,
- },
- ];
-
- glChartComponents.forEach((dynamicComponent) => {
- describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
- const findChartComponent = () => wrapper.findComponent(dynamicComponent.component);
-
- beforeEach(async () => {
- createWrapper(
- { graphData: timeSeriesGraphData({ type: dynamicComponent.chartType }) },
- mount,
- );
- await nextTick();
- });
-
- it('exists', () => {
- expect(findChartComponent().exists()).toBe(true);
- });
-
- it('receives data properties needed for proper chart render', () => {
- const props = findChartComponent().props();
-
- expect(props.data).toBe(wrapper.vm.chartData);
- expect(props.option).toBe(wrapper.vm.chartOptions);
- expect(props.formatTooltipText).toBe(wrapper.vm.formatTooltipText);
- });
-
- it('receives a tooltip title', async () => {
- const mockTitle = 'mockTitle';
- wrapper.vm.tooltip.title = mockTitle;
-
- await nextTick();
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltip-title', mockTitle),
- ).toBe(true);
- });
-
- describe('when tooltip is showing deployment data', () => {
- const mockSha = 'mockSha';
- const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
-
- beforeEach(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({
- tooltip: {
- type: 'deployments',
- },
- });
- await nextTick();
- });
-
- it('uses deployment title', () => {
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltip-title', 'Deployed'),
- ).toBe(true);
- });
-
- it('renders clickable commit sha in tooltip content', async () => {
- wrapper.vm.tooltip.sha = mockSha;
- wrapper.vm.tooltip.commitUrl = commitUrl;
-
- await nextTick();
- const commitLink = wrapper.findComponent(GlLink);
-
- expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
- expect(commitLink.attributes('href')).toEqual(commitUrl);
- });
- });
- });
- });
- });
- });
-
- describe('with multiple time series', () => {
- describe('General functions', () => {
- beforeEach(async () => {
- const graphData = timeSeriesGraphData({ type: panelTypes.AREA_CHART, multiMetric: true });
-
- createWrapper({ graphData }, mount);
- await nextTick();
- });
-
- describe('Color match', () => {
- let lineColors;
-
- beforeEach(() => {
- lineColors = wrapper
- .findComponent(GlAreaChart)
- .vm.series.map((item) => item.lineStyle.color);
- });
-
- it('should contain different colors for contiguous time series', () => {
- lineColors.forEach((color, index) => {
- expect(color).not.toBe(lineColors[index + 1]);
- });
- });
-
- it('should match series color with tooltip label color', () => {
- const labels = wrapper.findAllComponents(GlChartSeriesLabel);
-
- lineColors.forEach((color, index) => {
- const labelColor = labels.at(index).props('color');
- expect(color).toBe(labelColor);
- });
- });
-
- it('should match series color with legend color', () => {
- const legendColors = wrapper
- .findComponent(GlChartLegend)
- .props('seriesInfo')
- .map((item) => item.color);
-
- lineColors.forEach((color, index) => {
- expect(color).toBe(legendColors[index]);
- });
- });
- });
- });
- });
-
- describe('legend layout', () => {
- const findLegend = () => wrapper.findComponent(GlChartLegend);
-
- beforeEach(async () => {
- createWrapper({}, mount);
- await nextTick();
- });
-
- it('should render a tabular legend layout by default', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
-
- describe('when inline legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'inline',
- });
- });
-
- it('should render an inline legend layout', () => {
- expect(findLegend().props('layout')).toBe('inline');
- });
- });
-
- describe('when table legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'table',
- });
- });
-
- it('should render a tabular legend layout', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
deleted file mode 100644
index eb05b1f184a..00000000000
--- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
-
-describe('Create dashboard modal', () => {
- let wrapper;
-
- const defaultProps = {
- modalId: 'id',
- projectPath: 'https://localhost/',
- addDashboardDocumentationPath: 'https://link/to/docs',
- };
-
- const findDocsButton = () => wrapper.find('[data-testid="create-dashboard-modal-docs-button"]');
- const findRepoButton = () => wrapper.find('[data-testid="create-dashboard-modal-repo-button"]');
-
- const createWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(CreateDashboardModal, {
- propsData: { ...defaultProps, ...props },
- stubs: {
- GlModal,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- it('has button that links to the project url', async () => {
- findRepoButton().trigger('click');
-
- await nextTick();
- expect(findRepoButton().exists()).toBe(true);
- expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath);
- });
-
- it('has button that links to the docs', () => {
- expect(findDocsButton().exists()).toBe(true);
- expect(findDocsButton().attributes('href')).toBe(defaultProps.addDashboardDocumentationPath);
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
deleted file mode 100644
index 4d290922707..00000000000
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ /dev/null
@@ -1,421 +0,0 @@
-import { GlDropdownItem, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
-import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import Tracking from '~/tracking';
-import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
-import { setupAllDashboards, setupStoreWithData } from '../store_utils';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- queryToObject: jest.fn(),
-}));
-
-describe('Actions menu', () => {
- const ootbDashboards = [dashboardGitResponse[0], dashboardGitResponse[2]];
- const customDashboard = dashboardGitResponse[1];
-
- let store;
- let wrapper;
-
- const findAddMetricItem = () => wrapper.find('[data-testid="add-metric-item"]');
- const findAddPanelItemEnabled = () => wrapper.find('[data-testid="add-panel-item-enabled"]');
- const findAddPanelItemDisabled = () => wrapper.find('[data-testid="add-panel-item-disabled"]');
- const findAddMetricModal = () => wrapper.find('[data-testid="add-metric-modal"]');
- const findAddMetricModalSubmitButton = () =>
- wrapper.find('[data-testid="add-metric-modal-submit-button"]');
- const findStarDashboardItem = () => wrapper.find('[data-testid="star-dashboard-item"]');
- const findEditDashboardItemEnabled = () =>
- wrapper.find('[data-testid="edit-dashboard-item-enabled"]');
- const findEditDashboardItemDisabled = () =>
- wrapper.find('[data-testid="edit-dashboard-item-disabled"]');
- const findDuplicateDashboardItem = () => wrapper.find('[data-testid="duplicate-dashboard-item"]');
- const findDuplicateDashboardModal = () =>
- wrapper.find('[data-testid="duplicate-dashboard-modal"]');
- const findCreateDashboardItem = () => wrapper.find('[data-testid="create-dashboard-item"]');
- const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
-
- const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(ActionsMenu, {
- propsData: { ...dashboardActionsMenuProps, ...props },
- store,
- stubs: {
- GlModal,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- });
-
- describe('add metric item', () => {
- it('is rendered when custom metrics are available', async () => {
- createShallowWrapper();
-
- await nextTick();
- expect(findAddMetricItem().exists()).toBe(true);
- });
-
- it('is not rendered when custom metrics are not available', async () => {
- createShallowWrapper({
- addingMetricsAvailable: false,
- });
-
- await nextTick();
- expect(findAddMetricItem().exists()).toBe(false);
- });
-
- describe('when available', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('modal for custom metrics form is rendered', () => {
- expect(findAddMetricModal().exists()).toBe(true);
- expect(findAddMetricModal().props('modalId')).toBe('addMetric');
- });
-
- it('add metric modal submit button exists', () => {
- expect(findAddMetricModalSubmitButton().exists()).toBe(true);
- });
-
- it('renders custom metrics form fields', () => {
- expect(wrapper.findComponent(CustomMetricsFormFields).exists()).toBe(true);
- });
- });
-
- describe('when not available', () => {
- beforeEach(() => {
- createShallowWrapper({ addingMetricsAvailable: false });
- });
-
- it('modal for custom metrics form is not rendered', () => {
- expect(findAddMetricModal().exists()).toBe(false);
- });
- });
-
- describe('adding new metric from modal', () => {
- let origPage;
-
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockReturnValue();
- createShallowWrapper();
-
- setupStoreWithData(store);
-
- origPage = document.body.dataset.page;
- document.body.dataset.page = 'projects:environments:metrics';
-
- return nextTick();
- });
-
- afterEach(() => {
- document.body.dataset.page = origPage;
- });
-
- it('is tracked', async () => {
- const submitButton = findAddMetricModalSubmitButton().vm;
-
- await nextTick();
- submitButton.$el.click();
- await nextTick();
- expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'click_button', {
- label: 'add_new_metric',
- property: 'modal',
- value: undefined,
- });
- });
- });
- });
-
- describe('add panel item', () => {
- const GlDropdownItemStub = {
- extends: GlDropdownItem,
- props: {
- to: [String, Object],
- },
- };
-
- let $route;
-
- beforeEach(() => {
- $route = { name: DASHBOARD_PAGE, params: { dashboard: 'my_dashboard.yml' } };
-
- createShallowWrapper(
- {
- isOotbDashboard: false,
- },
- {
- mocks: { $route },
- stubs: { GlDropdownItem: GlDropdownItemStub },
- },
- );
- });
-
- it('is disabled for ootb dashboards', async () => {
- createShallowWrapper({
- isOotbDashboard: true,
- });
-
- await nextTick();
- expect(findAddPanelItemDisabled().exists()).toBe(true);
- });
-
- it('is visible for custom dashboards', () => {
- expect(findAddPanelItemEnabled().exists()).toBe(true);
- });
-
- it('renders a link to the new panel page for custom dashboards', () => {
- expect(findAddPanelItemEnabled().props('to')).toEqual({
- name: PANEL_NEW_PAGE,
- params: {
- dashboard: 'my_dashboard.yml',
- },
- });
- });
- });
-
- describe('edit dashboard yml item', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- describe('when current dashboard is custom', () => {
- beforeEach(() => {
- setupAllDashboards(store, customDashboard.path);
- });
-
- it('enabled item is rendered and has falsy disabled attribute', () => {
- expect(findEditDashboardItemEnabled().exists()).toBe(true);
- expect(findEditDashboardItemEnabled().attributes('disabled')).toBe(undefined);
- });
-
- it('enabled item links to their edit path', () => {
- expect(findEditDashboardItemEnabled().attributes('href')).toBe(
- customDashboard.project_blob_path,
- );
- });
-
- it('disabled item is not rendered', () => {
- expect(findEditDashboardItemDisabled().exists()).toBe(false);
- });
- });
-
- describe.each(ootbDashboards)('when current dashboard is OOTB', (dashboard) => {
- beforeEach(() => {
- setupAllDashboards(store, dashboard.path);
- });
-
- it('disabled item is rendered and has disabled attribute set on it', () => {
- expect(findEditDashboardItemDisabled().exists()).toBe(true);
- expect(findEditDashboardItemDisabled().attributes('disabled')).toBe('');
- });
-
- it('enabled item is not rendered', () => {
- expect(findEditDashboardItemEnabled().exists()).toBe(false);
- });
- });
- });
-
- describe('duplicate dashboard item', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- describe.each(ootbDashboards)('when current dashboard is OOTB', (dashboard) => {
- beforeEach(() => {
- setupAllDashboards(store, dashboard.path);
- });
-
- it('is rendered', () => {
- expect(findDuplicateDashboardItem().exists()).toBe(true);
- });
-
- it('duplicate dashboard modal is rendered', () => {
- expect(findDuplicateDashboardModal().exists()).toBe(true);
- });
-
- it('clicking on item opens up the duplicate dashboard modal', async () => {
- const modalId = 'duplicateDashboard';
- const modalTrigger = findDuplicateDashboardItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
-
- modalTrigger.trigger('click');
-
- await nextTick();
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
- });
- });
-
- describe('when current dashboard is custom', () => {
- beforeEach(() => {
- setupAllDashboards(store, customDashboard.path);
- });
-
- it('is not rendered', () => {
- expect(findDuplicateDashboardItem().exists()).toBe(false);
- });
-
- it('duplicate dashboard modal is not rendered', () => {
- expect(findDuplicateDashboardModal().exists()).toBe(false);
- });
- });
-
- describe('when no dashboard is set', () => {
- it('is not rendered', () => {
- expect(findDuplicateDashboardItem().exists()).toBe(false);
- });
-
- it('duplicate dashboard modal is not rendered', () => {
- expect(findDuplicateDashboardModal().exists()).toBe(false);
- });
- });
-
- describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = 'root/sandbox';
-
- setupAllDashboards(store, dashboardGitResponse[0].path);
- });
-
- it('redirects to the newly created dashboard', async () => {
- const newDashboard = dashboardGitResponse[1];
-
- const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
- findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
-
- await nextTick();
- expect(redirectTo).toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
- expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl); // eslint-disable-line import/no-deprecated
- });
- });
- });
-
- describe('star dashboard item', () => {
- beforeEach(() => {
- createShallowWrapper();
- setupAllDashboards(store);
-
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- it('is shown', () => {
- expect(findStarDashboardItem().exists()).toBe(true);
- });
-
- it('is not disabled', () => {
- expect(findStarDashboardItem().attributes('disabled')).toBeUndefined();
- });
-
- it('is disabled when starring is taking place', async () => {
- store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
-
- await nextTick();
- expect(findStarDashboardItem().exists()).toBe(true);
- expect(findStarDashboardItem().attributes('disabled')).toBeDefined();
- });
-
- it('on click it dispatches a toggle star action', async () => {
- findStarDashboardItem().vm.$emit('click');
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/toggleStarredValue',
- undefined,
- );
- });
-
- describe('when dashboard is not starred', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[0].path,
- });
- await nextTick();
- });
-
- it('item text shows "Star dashboard"', () => {
- expect(findStarDashboardItem().html()).toMatch(/Star dashboard/);
- });
- });
-
- describe('when dashboard is starred', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[1].path,
- });
- await nextTick();
- });
-
- it('item text shows "Unstar dashboard"', () => {
- expect(findStarDashboardItem().html()).toMatch(/Unstar dashboard/);
- });
- });
- });
-
- describe('create dashboard item', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('is rendered by default but it is disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBeDefined();
- });
-
- describe('when project path is set', () => {
- const mockProjectPath = 'root/sandbox';
- const mockAddDashboardDocPath = '/doc/add-dashboard';
-
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = mockProjectPath;
- store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
- });
-
- it('is not disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBe(undefined);
- });
-
- it('renders a modal for creating a dashboard', () => {
- expect(findCreateDashboardModal().exists()).toBe(true);
- });
-
- it('clicking opens up the modal', async () => {
- const modalId = 'createDashboard';
- const modalTrigger = findCreateDashboardItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
-
- modalTrigger.trigger('click');
-
- await nextTick();
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
- });
-
- it('modal gets passed correct props', () => {
- expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
- expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
- mockAddDashboardDocPath,
- );
- });
- });
-
- describe('when project path is not set', () => {
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = null;
- });
-
- it('is disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBeDefined();
- });
-
- it('does not render a modal for creating a dashboard', () => {
- expect(findCreateDashboardModal().exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
deleted file mode 100644
index 091e05ab271..00000000000
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ /dev/null
@@ -1,395 +0,0 @@
-import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import { environmentData, dashboardGitResponse, dashboardHeaderProps } from '../mock_data';
-import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
-
-const mockProjectPath = 'https://path/to/project';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- queryToObject: jest.fn(),
- mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
-}));
-
-describe('Dashboard header', () => {
- let store;
- let wrapper;
-
- const findDashboardDropdown = () => wrapper.findComponent(DashboardsDropdown);
-
- const findEnvsDropdown = () => wrapper.findComponent({ ref: 'monitorEnvironmentsDropdown' });
- const findEnvsDropdownItems = () => findEnvsDropdown().findAllComponents(GlDropdownItem);
- const findEnvsDropdownSearch = () => findEnvsDropdown().findComponent(GlSearchBoxByType);
- const findEnvsDropdownSearchMsg = () =>
- wrapper.findComponent({ ref: 'monitorEnvironmentsDropdownMsg' });
- const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().findComponent(GlLoadingIcon);
-
- const findDateTimePicker = () => wrapper.findComponent(DateTimePicker);
- const findRefreshButton = () => wrapper.findComponent(RefreshButton);
-
- const findActionsMenu = () => wrapper.findComponent(ActionsMenu);
-
- const setSearchTerm = (searchTerm) => {
- store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
- };
-
- const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(DashboardHeader, {
- propsData: { ...dashboardHeaderProps, ...props },
- store,
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- });
-
- describe('dashboards dropdown', () => {
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- projectPath: mockProjectPath,
- });
-
- createShallowWrapper();
- });
-
- it('shows the dashboard dropdown', () => {
- expect(findDashboardDropdown().exists()).toBe(true);
- });
-
- it('when an out of the box dashboard is selected, encodes dashboard path', () => {
- findDashboardDropdown().vm.$emit('selectDashboard', {
- path: '.gitlab/dashboards/dashboard&copy.yml',
- out_of_the_box_dashboard: true,
- display_name: 'A display name',
- });
-
- // eslint-disable-next-line import/no-deprecated
- expect(redirectTo).toHaveBeenCalledWith(
- `${mockProjectPath}/-/metrics/.gitlab%2Fdashboards%2Fdashboard%26copy.yml`,
- );
- });
-
- it('when a custom dashboard is selected, encodes dashboard display name', () => {
- findDashboardDropdown().vm.$emit('selectDashboard', {
- path: '.gitlab/dashboards/file&path.yml',
- display_name: 'dashboard&copy.yml',
- });
-
- // eslint-disable-next-line import/no-deprecated
- expect(redirectTo).toHaveBeenCalledWith(`${mockProjectPath}/-/metrics/dashboard%26copy.yml`);
- });
- });
-
- describe('environments dropdown', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('shows the environments dropdown', () => {
- expect(findEnvsDropdown().exists()).toBe(true);
- });
-
- it('renders a search input', () => {
- expect(findEnvsDropdownSearch().exists()).toBe(true);
- });
-
- describe('when environments data is not loaded', () => {
- beforeEach(async () => {
- setupStoreWithDashboard(store);
- await nextTick();
- });
-
- it('there are no environments listed', () => {
- expect(findEnvsDropdownItems()).toHaveLength(0);
- });
- });
-
- describe('when environments data is loaded', () => {
- const currentDashboard = dashboardGitResponse[0].path;
- const currentEnvironmentName = environmentData[0].name;
-
- beforeEach(async () => {
- setupStoreWithData(store);
- store.state.monitoringDashboard.projectPath = mockProjectPath;
- store.state.monitoringDashboard.currentDashboard = currentDashboard;
- store.state.monitoringDashboard.currentEnvironmentName = currentEnvironmentName;
-
- await nextTick();
- });
-
- it('renders dropdown items with the environment name', () => {
- const path = `${mockProjectPath}/-/metrics/${encodeURIComponent(currentDashboard)}`;
-
- findEnvsDropdownItems().wrappers.forEach((itemWrapper, index) => {
- const { name, id } = environmentData[index];
- const idParam = encodeURIComponent(id);
-
- expect(itemWrapper.text()).toBe(name);
- expect(itemWrapper.attributes('href')).toBe(`${path}?environment=${idParam}`);
- });
- });
-
- it('environments dropdown items can be checked', () => {
- const items = findEnvsDropdownItems();
- const checkItems = findEnvsDropdownItems().filter((item) => item.props('isCheckItem'));
-
- expect(items).toHaveLength(checkItems.length);
- });
-
- it('checks the currently selected environment', () => {
- const selectedItems = findEnvsDropdownItems().filter((item) => item.props('isChecked'));
-
- expect(selectedItems).toHaveLength(1);
- expect(selectedItems.at(0).text()).toBe(currentEnvironmentName);
- });
-
- it('filters rendered dropdown items', async () => {
- const searchTerm = 'production';
- const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
- setSearchTerm(searchTerm);
-
- await nextTick();
- expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
- });
-
- it('does not filter dropdown items if search term is empty string', async () => {
- const searchTerm = '';
- setSearchTerm(searchTerm);
-
- await nextTick();
- expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
- });
-
- it("shows error message if search term doesn't match", async () => {
- const searchTerm = 'does-not-exist';
- setSearchTerm(searchTerm);
-
- await nextTick();
- expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
- });
-
- it('shows loading element when environments fetch is still loading', async () => {
- store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
-
- await nextTick();
- expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
- await store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
- });
- });
- });
-
- describe('date time picker', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('is rendered', () => {
- expect(findDateTimePicker().exists()).toBe(true);
- });
-
- describe('timezone setting', () => {
- const setupWithTimezone = (value) => {
- store = createStore({ dashboardTimezone: value });
- createShallowWrapper();
- };
-
- describe('local timezone is enabled by default', () => {
- it('shows the data time picker in local timezone', () => {
- expect(findDateTimePicker().props('utc')).toBe(false);
- });
- });
-
- describe('when LOCAL timezone is enabled', () => {
- beforeEach(() => {
- setupWithTimezone('LOCAL');
- });
-
- it('shows the data time picker in local timezone', () => {
- expect(findDateTimePicker().props('utc')).toBe(false);
- });
- });
-
- describe('when UTC timezone is enabled', () => {
- beforeEach(() => {
- setupWithTimezone('UTC');
- });
-
- it('shows the data time picker in UTC format', () => {
- expect(findDateTimePicker().props('utc')).toBe(true);
- });
- });
- });
- });
-
- describe('refresh button', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('is rendered', () => {
- expect(findRefreshButton().exists()).toBe(true);
- });
- });
-
- describe('external dashboard link', () => {
- beforeEach(async () => {
- store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
- createShallowWrapper();
-
- await nextTick();
- });
-
- it('shows the link', () => {
- const externalDashboardButton = wrapper.find('.js-external-dashboard-link');
-
- expect(externalDashboardButton.exists()).toBe(true);
- expect(externalDashboardButton.is(GlButton)).toBe(true);
- expect(externalDashboardButton.text()).toContain('View full dashboard');
- });
- });
-
- describe('actions menu', () => {
- const ootbDashboards = [dashboardGitResponse[0].path];
- const customDashboards = [dashboardGitResponse[1].path];
-
- it('is rendered', () => {
- createShallowWrapper();
-
- expect(findActionsMenu().exists()).toBe(true);
- });
-
- describe('adding metrics prop', () => {
- it.each(ootbDashboards)(
- 'gets passed true if current dashboard is OOTB',
- async (dashboardPath) => {
- createShallowWrapper({ customMetricsAvailable: true });
-
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, dashboardPath);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
- },
- );
-
- it.each(customDashboards)(
- 'gets passed false if current dashboard is custom',
- async (dashboardPath) => {
- createShallowWrapper({ customMetricsAvailable: true });
-
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, dashboardPath);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- },
- );
-
- it('gets passed false if empty state is shown', async () => {
- createShallowWrapper({ customMetricsAvailable: true });
-
- store.state.monitoringDashboard.emptyState = true;
- setupAllDashboards(store, ootbDashboards[0]);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
-
- it('gets passed false if custom metrics are not available', async () => {
- createShallowWrapper({ customMetricsAvailable: false });
-
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, ootbDashboards[0]);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
- });
-
- it('custom metrics path gets passed', async () => {
- const path = 'https://path/to/customMetrics';
-
- createShallowWrapper({ customMetricsPath: path });
-
- await nextTick();
- expect(findActionsMenu().props('customMetricsPath')).toBe(path);
- });
-
- it('validate query path gets passed', async () => {
- const path = 'https://path/to/validateQuery';
-
- createShallowWrapper({ validateQueryPath: path });
-
- await nextTick();
- expect(findActionsMenu().props('validateQueryPath')).toBe(path);
- });
-
- it('default branch gets passed', async () => {
- const branch = 'branchName';
-
- createShallowWrapper({ defaultBranch: branch });
-
- await nextTick();
- expect(findActionsMenu().props('defaultBranch')).toBe(branch);
- });
- });
-
- describe('metrics settings button', () => {
- const findSettingsButton = () => wrapper.find('[data-testid="metrics-settings-button"]');
- const url = 'https://path/to/project/settings';
-
- beforeEach(() => {
- createShallowWrapper();
-
- store.state.monitoringDashboard.canAccessOperationsSettings = false;
- store.state.monitoringDashboard.operationsSettingsPath = '';
- });
-
- it('is rendered when the user can access the project settings and path to settings is available', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = true;
- store.state.monitoringDashboard.operationsSettingsPath = url;
-
- await nextTick();
- expect(findSettingsButton().exists()).toBe(true);
- });
-
- it('is not rendered when the user can not access the project settings', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = false;
- store.state.monitoringDashboard.operationsSettingsPath = url;
-
- await nextTick();
- expect(findSettingsButton().exists()).toBe(false);
- });
-
- it('is not rendered when the path to settings is unavailable', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = false;
- store.state.monitoringDashboard.operationsSettingsPath = '';
-
- await nextTick();
- expect(findSettingsButton().exists()).toBe(false);
- });
-
- it('leads to the project settings page', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = true;
- store.state.monitoringDashboard.operationsSettingsPath = url;
-
- await nextTick();
- expect(findSettingsButton().attributes('href')).toBe(url);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
deleted file mode 100644
index 1cfd132b123..00000000000
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ /dev/null
@@ -1,226 +0,0 @@
-import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import { metricsDashboardResponse } from '../fixture_data';
-import { mockTimeRange } from '../mock_data';
-
-const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0];
-
-describe('dashboard invalid url parameters', () => {
- let store;
- let wrapper;
- let mockShowToast;
-
- const createComponent = (props = {}, options = {}) => {
- wrapper = shallowMount(DashboardPanelBuilder, {
- propsData: { ...props },
- store,
- stubs: {
- GlCard,
- },
- mocks: {
- $toast: {
- show: mockShowToast,
- },
- },
- options,
- });
- };
-
- const findForm = () => wrapper.findComponent(GlForm);
- const findTxtArea = () => findForm().findComponent(GlFormTextarea);
- const findSubmitBtn = () => findForm().find('[type="submit"]');
- const findClipboardCopyBtn = () => wrapper.findComponent({ ref: 'clipboardCopyBtn' });
- const findViewDocumentationBtn = () => wrapper.findComponent({ ref: 'viewDocumentationBtn' });
- const findOpenRepositoryBtn = () => wrapper.findComponent({ ref: 'openRepositoryBtn' });
- const findPanel = () => wrapper.findComponent(DashboardPanel);
- const findTimeRangePicker = () => wrapper.findComponent(DateTimePicker);
- const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]');
-
- beforeEach(() => {
- mockShowToast = jest.fn();
- store = createStore();
- createComponent();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- it('is mounted', () => {
- expect(wrapper.exists()).toBe(true);
- });
-
- it('displays an empty dashboard panel', () => {
- expect(findPanel().exists()).toBe(true);
- expect(findPanel().props('graphData')).toBe(null);
- });
-
- it('does not fetch initial data by default', () => {
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-
- describe('yml form', () => {
- it('form exists and can be submitted', () => {
- expect(findForm().exists()).toBe(true);
- expect(findSubmitBtn().exists()).toBe(true);
- expect(findSubmitBtn().props('disabled')).toBe(false);
- });
-
- it('form has a text area with a default value', () => {
- expect(findTxtArea().exists()).toBe(true);
-
- const value = findTxtArea().attributes('value');
-
- // Panel definition should contain a title and a type
- expect(value).toContain('title:');
- expect(value).toContain('type:');
- });
-
- it('"copy to clipboard" button works', () => {
- findClipboardCopyBtn().vm.$emit('click');
- const clipboardText = findClipboardCopyBtn().attributes('data-clipboard-text');
-
- expect(clipboardText).toContain('title:');
- expect(clipboardText).toContain('type:');
-
- expect(mockShowToast).toHaveBeenCalledTimes(1);
- });
-
- it('on submit fetches a panel preview', async () => {
- findForm().vm.$emit('submit', new Event('submit'));
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/fetchPanelPreview',
- expect.stringContaining('title:'),
- );
- });
-
- describe('when form is submitted', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content');
- await nextTick();
- });
-
- it('submit button is disabled', () => {
- expect(findSubmitBtn().props('disabled')).toBe(true);
- });
- });
- });
-
- describe('time range picker', () => {
- it('is visible by default', () => {
- expect(findTimeRangePicker().exists()).toBe(true);
- });
-
- it('when changed does not trigger data fetch unless preview panel button is clicked', async () => {
- // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-
- it('when changed triggers data fetch if preview panel button is clicked', async () => {
- findForm().vm.$emit('submit', new Event('submit'));
-
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalled();
- });
- });
-
- describe('refresh', () => {
- it('is visible by default', () => {
- expect(findRefreshButton().exists()).toBe(true);
- });
-
- it('when clicked does not trigger data fetch unless preview panel button is clicked', async () => {
- // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-
- it('when clicked triggers data fetch if preview panel button is clicked', async () => {
- // mimic state where preview is visible. SET_PANEL_PREVIEW_IS_SHOWN is set to true
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, true);
-
- findRefreshButton().vm.$emit('click');
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/fetchPanelPreviewMetrics',
- undefined,
- );
- });
- });
-
- describe('instructions card', () => {
- const mockDocsPath = '/docs-path';
- const mockProjectPath = '/project-path';
-
- beforeEach(() => {
- store.state.monitoringDashboard.addDashboardDocumentationPath = mockDocsPath;
- store.state.monitoringDashboard.projectPath = mockProjectPath;
-
- createComponent();
- });
-
- it('displays next actions for the user', () => {
- expect(findViewDocumentationBtn().exists()).toBe(true);
- expect(findViewDocumentationBtn().attributes('href')).toBe(mockDocsPath);
-
- expect(findOpenRepositoryBtn().exists()).toBe(true);
- expect(findOpenRepositoryBtn().attributes('href')).toBe(mockProjectPath);
- });
- });
-
- describe('when there is an error', () => {
- const mockError = 'an error occurred!';
-
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError);
- await nextTick();
- });
-
- it('displays an alert', () => {
- expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
- expect(wrapper.findComponent(GlAlert).text()).toBe(mockError);
- });
-
- it('displays an empty dashboard panel', () => {
- expect(findPanel().props('graphData')).toBe(null);
- });
-
- it('changing time range should not refetch data', async () => {
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalled();
- });
- });
-
- describe('when panel data is available', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel);
- await nextTick();
- });
-
- it('displays no alert', () => {
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
- });
-
- it('displays panel with data', () => {
- const { title, type } = wrapper.findComponent(DashboardPanel).props('graphData');
-
- expect(title).toBe(mockPanel.title);
- expect(type).toBe(mockPanel.type);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
deleted file mode 100644
index 491649e5b96..00000000000
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ /dev/null
@@ -1,582 +0,0 @@
-import { GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import axios from '~/lib/utils/axios_utils';
-
-import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
-import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
-import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
-import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
-import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import { panelTypes } from '~/monitoring/constants';
-
-import { createStore, monitoringDashboard } from '~/monitoring/stores';
-import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
-import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import {
- anomalyGraphData,
- singleStatGraphData,
- heatmapGraphData,
- barGraphData,
-} from '../graph_data';
-import { mockNamespace, mockNamespacedData, mockTimeRange } from '../mock_data';
-
-const mocks = {
- $toast: {
- show: jest.fn(),
- },
-};
-
-describe('Dashboard Panel', () => {
- let axiosMock;
- let store;
- let state;
- let wrapper;
-
- const exampleText = 'example_text';
-
- const findCopyLink = () => wrapper.findComponent({ ref: 'copyChartLink' });
- const findTimeChart = () => wrapper.findComponent({ ref: 'timeSeriesChart' });
- const findTitle = () => wrapper.findComponent({ ref: 'graphTitle' });
- const findCtxMenu = () => wrapper.findComponent({ ref: 'contextualMenu' });
- const findMenuItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text);
-
- const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
- wrapper = mountFn(DashboardPanel, {
- propsData: {
- graphData,
- settingsPath: dashboardProps.settingsPath,
- ...props,
- },
- store,
- mocks,
- ...options,
- });
- };
-
- const mockGetterReturnValue = (getter, value) => {
- jest.spyOn(monitoringDashboard.getters, getter).mockReturnValue(value);
- store = new Vuex.Store({
- modules: {
- monitoringDashboard,
- },
- });
- };
-
- beforeEach(() => {
- store = createStore();
- state = store.state.monitoringDashboard;
-
- axiosMock = new AxiosMockAdapter(axios);
-
- jest.spyOn(URL, 'createObjectURL');
- });
-
- afterEach(() => {
- axiosMock.reset();
- });
-
- describe('Renders slots', () => {
- it('renders "topLeft" slot', () => {
- createWrapper(
- {},
- {
- slots: {
- 'top-left': `<div class="top-left-content">OK</div>`,
- },
- },
- );
-
- expect(wrapper.find('.top-left-content').exists()).toBe(true);
- expect(wrapper.find('.top-left-content').text()).toBe('OK');
- });
- });
-
- describe('When no graphData is available', () => {
- beforeEach(() => {
- createWrapper({
- graphData: graphDataEmpty,
- });
- });
-
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphDataEmpty.title);
- });
-
- it('renders no download csv link', () => {
- expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
-
- it('does not contain graph widgets', () => {
- expect(findCtxMenu().exists()).toBe(false);
- });
-
- it('The Empty Chart component is rendered and is a Vue instance', () => {
- expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
- });
- });
-
- describe('When graphData is null', () => {
- beforeEach(() => {
- createWrapper({
- graphData: null,
- });
- });
-
- it('renders no chart title', () => {
- expect(findTitle().text()).toBe('');
- });
-
- it('renders no download csv link', () => {
- expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
-
- it('does not contain graph widgets', () => {
- expect(findCtxMenu().exists()).toBe(false);
- });
-
- it('The Empty Chart component is rendered and is a Vue instance', () => {
- expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
- });
- });
-
- describe('When graphData is available', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphData.title);
- });
-
- it('contains graph widgets', () => {
- expect(findCtxMenu().exists()).toBe(true);
- expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(true);
- });
-
- it('sets no clipboard copy link on dropdown by default', () => {
- expect(findCopyLink().exists()).toBe(false);
- });
-
- it('should emit `timerange` event when a zooming in/out in a chart occcurs', async () => {
- const timeRange = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- };
-
- jest.spyOn(wrapper.vm, '$emit');
-
- findTimeChart().vm.$emit('datazoom', timeRange);
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
- });
-
- it('includes a default group id', () => {
- expect(wrapper.vm.groupId).toBe('dashboard-panel');
- });
-
- describe('Supports different panel types', () => {
- const dataWithType = (type) => {
- return {
- ...graphData,
- type,
- };
- };
-
- it('empty chart is rendered for empty results', () => {
- createWrapper({ graphData: graphDataEmpty });
- expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
- });
-
- it('area chart is rendered by default', () => {
- createWrapper();
- expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
- });
-
- describe.each`
- data | component | hasCtxMenu
- ${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart} | ${true}
- ${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart} | ${true}
- ${singleStatGraphData()} | ${MonitorSingleStatChart} | ${true}
- ${anomalyGraphData()} | ${MonitorAnomalyChart} | ${false}
- ${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
- ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
- ${heatmapGraphData()} | ${MonitorHeatmapChart} | ${false}
- ${barGraphData()} | ${MonitorBarChart} | ${false}
- `('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
- const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
-
- beforeEach(() => {
- createWrapper({ graphData: data }, { attrs });
- });
-
- it(`renders the chart component and binds attributes`, () => {
- expect(wrapper.findComponent(component).exists()).toBe(true);
- expect(wrapper.findComponent(component).attributes()).toMatchObject(attrs);
- });
-
- it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => {
- expect(findCtxMenu().exists()).toBe(hasCtxMenu);
- });
- });
- });
-
- describe('computed', () => {
- describe('fixedCurrentTimeRange', () => {
- it('returns fixed time for valid time range', async () => {
- state.timeRange = mockTimeRange;
- await nextTick();
- expect(findTimeChart().props('timeRange')).toEqual(
- expect.objectContaining({
- start: expect.any(String),
- end: expect.any(String),
- }),
- );
- });
-
- it.each`
- input | output
- ${''} | ${{}}
- ${undefined} | ${{}}
- ${null} | ${{}}
- ${'2020-12-03'} | ${{}}
- `('returns $output for invalid input like $input', async ({ input, output }) => {
- state.timeRange = input;
- await nextTick();
- expect(findTimeChart().props('timeRange')).toEqual(output);
- });
- });
- });
- });
-
- describe('Edit custom metric dropdown item', () => {
- const findEditCustomMetricLink = () => wrapper.findComponent({ ref: 'editMetricLink' });
- const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
-
- beforeEach(async () => {
- createWrapper();
- await nextTick();
- });
-
- it('is not present if the panel is not a custom metric', () => {
- expect(findEditCustomMetricLink().exists()).toBe(false);
- });
-
- it('is present when the panel contains an edit_path property', async () => {
- wrapper.setProps({
- graphData: {
- ...graphData,
- metrics: [
- {
- ...graphData.metrics[0],
- edit_path: mockEditPath,
- },
- ],
- },
- });
-
- await nextTick();
- expect(findEditCustomMetricLink().exists()).toBe(true);
- expect(findEditCustomMetricLink().text()).toBe('Edit metric');
- expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
- });
-
- it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', async () => {
- wrapper.setProps({
- graphData: {
- ...graphData,
- metrics: [
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- ],
- },
- });
-
- await nextTick();
- expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
- expect(findEditCustomMetricLink().attributes('href')).toBe(dashboardProps.settingsPath);
- });
- });
-
- describe('when clipboard data is available', () => {
- const clipboardText = 'A value to copy.';
-
- beforeEach(() => {
- createWrapper({
- clipboardText,
- });
- });
-
- it('sets clipboard text on the dropdown', () => {
- expect(findCopyLink().exists()).toBe(true);
- expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
- });
-
- it('adds a copy button to the dropdown', () => {
- expect(findCopyLink().text()).toContain('Copy link to chart');
- });
-
- it('opens a toast on click', () => {
- findCopyLink().vm.$emit('click');
-
- expect(wrapper.vm.$toast.show).toHaveBeenCalled();
- });
- });
-
- describe('when clipboard data is not available', () => {
- it('there is no "copy to clipboard" link for a null value', () => {
- createWrapper({ clipboardText: null });
- expect(findCopyLink().exists()).toBe(false);
- });
-
- it('there is no "copy to clipboard" link for an empty value', () => {
- createWrapper({ clipboardText: '' });
- expect(findCopyLink().exists()).toBe(false);
- });
- });
-
- describe('when downloading metrics data as CSV', () => {
- beforeEach(async () => {
- wrapper = shallowMount(DashboardPanel, {
- propsData: {
- clipboardText: exampleText,
- settingsPath: dashboardProps.settingsPath,
- graphData: {
- y_label: 'metric',
- ...graphData,
- },
- },
- store,
- });
- await nextTick();
- });
-
- describe('csvText', () => {
- it('converts metrics data from json to csv', () => {
- const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`;
- const data = graphData.metrics[0].result[0].values;
- const firstRow = `${data[0][0]},${data[0][1]}`;
- const secondRow = `${data[1][0]},${data[1][1]}`;
-
- expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
- });
- });
-
- describe('downloadCsv', () => {
- it('produces a link with a Blob', () => {
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
- expect.objectContaining({
- size: wrapper.vm.csvText.length,
- type: 'text/plain',
- }),
- );
- });
- });
- });
-
- describe('when using dynamic modules', () => {
- const { mockDeploymentData, mockProjectPath } = mockNamespacedData;
-
- beforeEach(() => {
- store = createEmbedGroupStore();
- store.registerModule(mockNamespace, monitoringDashboard);
- store.state.embedGroup.modules.push(mockNamespace);
-
- createWrapper({ namespace: mockNamespace });
- });
-
- it('handles namespaced deployment data state', async () => {
- store.state[mockNamespace].deploymentData = mockDeploymentData;
-
- await nextTick();
- expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
- });
-
- it('handles namespaced project path state', async () => {
- store.state[mockNamespace].projectPath = mockProjectPath;
-
- await nextTick();
- expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
- });
-
- it('renders a time series chart with no errors', () => {
- expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
- });
- });
-
- describe('panel timezone', () => {
- it('displays a time chart in local timezone', () => {
- createWrapper();
- expect(findTimeChart().props('timezone')).toBe('LOCAL');
- });
-
- it('displays a heatmap in local timezone', () => {
- createWrapper({ graphData: heatmapGraphData() });
- expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
- });
-
- describe('when timezone is set to UTC', () => {
- beforeEach(() => {
- store = createStore({ dashboardTimezone: 'UTC' });
- });
-
- it('displays a time chart with UTC', () => {
- createWrapper();
- expect(findTimeChart().props('timezone')).toBe('UTC');
- });
-
- it('displays a heatmap with UTC', () => {
- createWrapper({ graphData: heatmapGraphData() });
- expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('UTC');
- });
- });
- });
-
- describe('Expand to full screen', () => {
- const findExpandBtn = () => wrapper.findComponent({ ref: 'expandBtn' });
-
- describe('when there is no @expand listener', () => {
- it('does not show `View full screen` option', () => {
- createWrapper();
- expect(findExpandBtn().exists()).toBe(false);
- });
- });
-
- describe('when there is an @expand listener', () => {
- beforeEach(() => {
- createWrapper({}, { listeners: { expand: () => {} } });
- });
-
- it('shows the `expand` option', () => {
- expect(findExpandBtn().exists()).toBe(true);
- });
-
- it('emits the `expand` event', () => {
- const preventDefault = jest.fn();
- findExpandBtn().vm.$emit('click', { preventDefault });
- expect(wrapper.emitted('expand')).toHaveLength(1);
- expect(preventDefault).toHaveBeenCalled();
- });
- });
- });
-
- describe('When graphData contains links', () => {
- const findManageLinksItem = () => wrapper.findComponent({ ref: 'manageLinksItem' });
- const mockLinks = [
- {
- url: 'https://example.com',
- title: 'Example 1',
- },
- {
- url: 'https://gitlab.com',
- title: 'Example 2',
- },
- ];
- const createWrapperWithLinks = (links = mockLinks) => {
- createWrapper({
- graphData: {
- ...graphData,
- links,
- },
- });
- };
-
- it('custom links are shown', () => {
- createWrapperWithLinks();
-
- mockLinks.forEach(({ url, title }) => {
- const link = findMenuItemByText(title).at(0);
-
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(url);
- });
- });
-
- it("custom links don't show unsecure content", () => {
- createWrapperWithLinks([
- {
- title: '<script>alert("XSS")</script>',
- url: 'http://example.com',
- },
- ]);
-
- expect(findMenuItems().at(1).element.innerHTML).toBe(
- '&lt;script&gt;alert("XSS")&lt;/script&gt;',
- );
- });
-
- it("custom links don't show unsecure href attributes", () => {
- const title = 'Owned!';
-
- createWrapperWithLinks([
- {
- title,
- // eslint-disable-next-line no-script-url
- url: 'javascript:alert("Evil")',
- },
- ]);
-
- const link = findMenuItemByText(title).at(0);
- expect(link.attributes('href')).toBe('#');
- });
-
- it('when an editable dashboard is selected, shows `Manage chart links` link to the blob path', () => {
- const editUrl = '/edit';
- mockGetterReturnValue('selectedDashboard', {
- can_edit: true,
- project_blob_path: editUrl,
- });
- createWrapperWithLinks();
-
- expect(findManageLinksItem().exists()).toBe(true);
- expect(findManageLinksItem().attributes('href')).toBe(editUrl);
- });
-
- it('when no dashboard is selected, does not show `Manage chart links`', () => {
- mockGetterReturnValue('selectedDashboard', null);
- createWrapperWithLinks();
-
- expect(findManageLinksItem().exists()).toBe(false);
- });
-
- it('when non-editable dashboard is selected, does not show `Manage chart links`', () => {
- const editUrl = '/edit';
- mockGetterReturnValue('selectedDashboard', {
- can_edit: false,
- project_blob_path: editUrl,
- });
- createWrapperWithLinks();
-
- expect(findManageLinksItem().exists()).toBe(false);
- });
- });
-
- describe('Runbook url', () => {
- const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
-
- beforeEach(() => {
- mockGetterReturnValue('metricsSavedToDb', []);
- });
-
- it('does not show a runbook link when alerts are not present', () => {
- createWrapper();
-
- expect(findRunbookLinks().length).toBe(0);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
deleted file mode 100644
index d7f1d4873bb..00000000000
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ /dev/null
@@ -1,784 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import VueDraggable from 'vuedraggable';
-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 '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { objectToQuery } from '~/lib/utils/url_utility';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import EmptyState from '~/monitoring/components/empty_state.vue';
-import GraphGroup from '~/monitoring/components/graph_group.vue';
-import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
-import LinksSection from '~/monitoring/components/links_section.vue';
-import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import {
- metricsDashboardViewModel,
- metricsDashboardPanelCount,
- dashboardProps,
-} from '../fixture_data';
-import { dashboardGitResponse, storeVariables } from '../mock_data';
-import {
- setupAllDashboards,
- setupStoreWithDashboard,
- setMetricResult,
- setupStoreWithData,
- setupStoreWithDataForPanelCount,
- setupStoreWithLinks,
-} from '../store_utils';
-
-jest.mock('~/alert');
-
-describe('Dashboard', () => {
- let store;
- let wrapper;
- let mock;
-
- const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMountExtended(Dashboard, {
- propsData: { ...dashboardProps, ...props },
- store,
- stubs: {
- DashboardHeader,
- },
- ...options,
- });
- };
-
- const createMountedWrapper = (props = {}, options = {}) => {
- wrapper = mountExtended(Dashboard, {
- propsData: { ...dashboardProps, ...props },
- store,
- stubs: {
- 'graph-group': true,
- 'dashboard-panel': true,
- 'dashboard-header': DashboardHeader,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- mock = new MockAdapter(axios);
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- afterEach(() => {
- mock.restore();
- if (store.dispatch.mockReset) {
- store.dispatch.mockReset();
- }
- });
-
- describe('request information to the server', () => {
- it('calls to set time range and fetch data', async () => {
- createShallowWrapper({ hasMetrics: true });
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.any(Object),
- );
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('shows up a loading state', async () => {
- store.state.monitoringDashboard.emptyState = dashboardEmptyStates.LOADING;
-
- createShallowWrapper({ hasMetrics: true });
-
- await nextTick();
- expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
- expect(wrapper.findComponent(EmptyState).props('selectedState')).toBe(
- dashboardEmptyStates.LOADING,
- );
- });
-
- it('hides the group panels when showPanels is false', async () => {
- createMountedWrapper({ hasMetrics: true, showPanels: false });
-
- setupStoreWithData(store);
-
- await nextTick();
- expect(wrapper.vm.emptyState).toBeNull();
- expect(wrapper.findAll('.prometheus-panel')).toHaveLength(0);
- });
-
- it('fetches the metrics data with proper time window', async () => {
- createMountedWrapper({ hasMetrics: true });
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.objectContaining({ duration: { seconds: 28800 } }),
- );
- });
- });
-
- describe('panel containers layout', () => {
- const findPanelLayoutWrapperAt = (index) => {
- return wrapper
- .findComponent(GraphGroup)
- .findAll('[data-testid="dashboard-panel-layout-wrapper"]')
- .at(index);
- };
-
- beforeEach(async () => {
- createMountedWrapper({ hasMetrics: true });
- await nextTick();
- });
-
- describe('when the graph group has an even number of panels', () => {
- it('2 panels - all panel wrappers take half width of their parent', async () => {
- setupStoreWithDataForPanelCount(store, 2);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- });
-
- it('4 panels - all panel wrappers take half width of their parent', async () => {
- setupStoreWithDataForPanelCount(store, 4);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
- });
- });
-
- describe('when the graph group has an odd number of panels', () => {
- it('1 panel - panel wrapper does not take half width of its parent', async () => {
- setupStoreWithDataForPanelCount(store, 1);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(false);
- });
-
- it('3 panels - all panels but last take half width of their parents', async () => {
- setupStoreWithDataForPanelCount(store, 3);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(false);
- });
-
- it('5 panels - all panels but last take half width of their parents', async () => {
- setupStoreWithDataForPanelCount(store, 5);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(4).classes('col-lg-6')).toBe(false);
- });
- });
- });
-
- describe('dashboard validation warning', () => {
- it('displays a warning if there are validation warnings', async () => {
- createMountedWrapper({ hasMetrics: true });
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS}`,
- true,
- );
-
- await nextTick();
- expect(createAlert).toHaveBeenCalled();
- });
-
- it('does not display a warning if there are no validation warnings', async () => {
- createMountedWrapper({ hasMetrics: true });
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS}`,
- false,
- );
-
- await nextTick();
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
-
- describe('when the URL contains a reference to a panel', () => {
- const location = window.location.href;
-
- const setSearch = (searchParams) => {
- setWindowLocation(`?${objectToQuery(searchParams)}`);
- };
-
- afterEach(() => {
- setWindowLocation(location);
- });
-
- it('when the URL points to a panel it expands', async () => {
- const panelGroup = metricsDashboardViewModel.panelGroups[0];
- const panel = panelGroup.panels[0];
-
- setSearch({
- group: panelGroup.group,
- title: panel.title,
- y_label: panel.y_label,
- });
-
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
- group: panelGroup.group,
- panel: expect.objectContaining({
- title: panel.title,
- y_label: panel.y_label,
- }),
- });
- });
-
- it('when the URL does not link to any panel, no panel is expanded', async () => {
- setSearch();
-
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'monitoringDashboard/setExpandedPanel',
- expect.anything(),
- );
- });
-
- it('when the URL points to an incorrect panel it shows an error', async () => {
- const panelGroup = metricsDashboardViewModel.panelGroups[0];
- const panel = panelGroup.panels[0];
-
- setSearch({
- group: panelGroup.group,
- title: 'incorrect',
- y_label: panel.y_label,
- });
-
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- expect(createAlert).toHaveBeenCalled();
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'monitoringDashboard/setExpandedPanel',
- expect.anything(),
- );
- });
- });
-
- describe('when the panel is expanded', () => {
- let group;
- let panel;
-
- const expandPanel = (mockGroup, mockPanel) => {
- store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
- group: mockGroup,
- panel: mockPanel,
- });
- };
-
- beforeEach(() => {
- setupStoreWithData(store);
-
- const { panelGroups } = store.state.monitoringDashboard.dashboard;
- group = panelGroups[0].group;
- [panel] = panelGroups[0].panels;
-
- jest.spyOn(window.history, 'pushState').mockImplementation();
- });
-
- afterEach(() => {
- window.history.pushState.mockRestore();
- });
-
- it('URL is updated with panel parameters', async () => {
- createMountedWrapper({ hasMetrics: true });
- expandPanel(group, panel);
-
- const expectedSearch = objectToQuery({
- group,
- title: panel.title,
- y_label: panel.y_label,
- });
-
- await nextTick();
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.stringContaining(`${expectedSearch}`),
- );
- });
-
- it('URL is updated with panel parameters and custom dashboard', async () => {
- const dashboard = 'dashboard.yml';
-
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboard,
- });
- createMountedWrapper({ hasMetrics: true });
- expandPanel(group, panel);
-
- const expectedSearch = objectToQuery({
- dashboard,
- group,
- title: panel.title,
- y_label: panel.y_label,
- });
-
- await nextTick();
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.stringContaining(`${expectedSearch}`),
- );
- });
-
- it('URL is updated with no parameters', async () => {
- expandPanel(group, panel);
- createMountedWrapper({ hasMetrics: true });
- expandPanel(null, null);
-
- await nextTick();
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.not.stringMatching(/group|title|y_label/), // no panel params
- );
- });
- });
-
- describe('when all panels in the first group are loading', () => {
- const findGroupAt = (i) => wrapper.findAllComponents(GraphGroup).at(i);
-
- beforeEach(async () => {
- setupStoreWithDashboard(store);
-
- const { panels } = store.state.monitoringDashboard.dashboard.panelGroups[0];
- panels.forEach(({ metrics }) => {
- store.commit(`monitoringDashboard/${types.REQUEST_METRIC_RESULT}`, {
- metricId: metrics[0].metricId,
- });
- });
-
- createShallowWrapper();
-
- await nextTick();
- });
-
- it('a loading icon appears in the first group', () => {
- expect(findGroupAt(0).props('isLoading')).toBe(true);
- });
-
- it('a loading icon does not appear in the second group', () => {
- expect(findGroupAt(1).props('isLoading')).toBe(false);
- });
- });
-
- describe('when all requests have been committed by the store', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentEnvironmentName: 'production',
- currentDashboard: dashboardGitResponse[0].path,
- projectPath: TEST_HOST,
- });
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- });
-
- it('does not show loading icons in any group', async () => {
- setupStoreWithData(store);
-
- await nextTick();
- wrapper.findAllComponents(GraphGroup).wrappers.forEach((groupWrapper) => {
- expect(groupWrapper.props('isLoading')).toBe(false);
- });
- });
- });
-
- describe('variables section', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- store.state.monitoringDashboard.variables = storeVariables;
- await nextTick();
- });
-
- it('shows the variables section', () => {
- expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
- });
- });
-
- describe('links section', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- setupStoreWithLinks(store);
- await nextTick();
- });
-
- it('shows the links section', () => {
- expect(wrapper.vm.shouldShowLinksSection).toBe(true);
- expect(wrapper.findComponent(LinksSection).exists()).toBe(true);
- });
- });
-
- describe('single panel expands to "full screen" mode', () => {
- const findExpandedPanel = () => wrapper.findComponent({ ref: 'expandedPanel' });
-
- describe('when the panel is not expanded', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- await nextTick();
- });
-
- it('expanded panel is not visible', () => {
- expect(findExpandedPanel().isVisible()).toBe(false);
- });
-
- it('can set a panel as expanded', () => {
- const panel = wrapper.findAllComponents(DashboardPanel).at(1);
-
- jest.spyOn(store, 'dispatch');
-
- panel.vm.$emit('expand');
-
- const groupData = metricsDashboardViewModel.panelGroups[0];
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
- group: groupData.group,
- panel: expect.objectContaining({
- id: groupData.panels[0].id,
- }),
- });
- });
- });
-
- describe('when the panel is expanded', () => {
- let group;
- let panel;
-
- const MockPanel = {
- template: `<div><slot name="top-left"/></div>`,
- };
-
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
- setupStoreWithData(store);
-
- const { panelGroups } = store.state.monitoringDashboard.dashboard;
-
- group = panelGroups[0].group;
- [panel] = panelGroups[0].panels;
-
- store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
- group,
- panel,
- });
-
- jest.spyOn(store, 'dispatch');
- await nextTick();
- });
-
- it('displays a single panel and others are hidden', () => {
- const panels = wrapper.findAllComponents(MockPanel);
- const visiblePanels = panels.filter((w) => w.isVisible());
-
- expect(findExpandedPanel().isVisible()).toBe(true);
- // v-show for hiding panels is more performant than v-if
- // check for panels to be hidden.
- expect(panels.length).toBe(metricsDashboardPanelCount + 1);
- expect(visiblePanels.length).toBe(1);
- });
-
- it('sets a link to the expanded panel', () => {
- const searchQuery =
- '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
-
- expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
- expect.stringContaining(searchQuery),
- );
- });
-
- it('restores full dashboard by clicking `back`', () => {
- wrapper.findComponent({ ref: 'goBackBtn' }).vm.$emit('click');
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/clearExpandedPanel',
- undefined,
- );
- });
- });
- });
-
- describe('when one of the metrics is missing', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
-
- setupStoreWithDashboard(store);
- setMetricResult({ store, result: [], panel: 2 });
- await nextTick();
- });
-
- it('shows a group empty area', () => {
- const emptyGroup = wrapper.findAllComponents({ ref: 'empty-group' });
-
- expect(emptyGroup).toHaveLength(1);
- expect(emptyGroup.is(GroupEmptyState)).toBe(true);
- });
-
- it('group empty area displays a NO_DATA state', () => {
- expect(
- wrapper.findAllComponents({ ref: 'empty-group' }).at(0).props('selectedState'),
- ).toEqual(metricStates.NO_DATA);
- });
- });
-
- describe('drag and drop function', () => {
- const findDraggables = () => wrapper.findAllComponents(VueDraggable);
- const findEnabledDraggables = () => findDraggables().filter((f) => !f.attributes('disabled'));
- const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
- const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
-
- const setup = async () => {
- // call original dispatch
- store.dispatch.mockRestore();
-
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- await nextTick();
- };
-
- it('wraps vuedraggable', async () => {
- await setup();
-
- expect(findDraggablePanels().exists()).toBe(true);
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount);
- });
-
- it('is disabled by default', async () => {
- await setup();
-
- expect(findRearrangeButton().exists()).toBe(false);
- expect(findEnabledDraggables().length).toBe(0);
- });
-
- describe('when rearrange is enabled', () => {
- beforeEach(async () => {
- // call original dispatch
- store.dispatch.mockRestore();
-
- createShallowWrapper({ hasMetrics: true, rearrangePanelsAvailable: true });
- setupStoreWithData(store);
-
- await nextTick();
- });
-
- it('displays rearrange button', () => {
- expect(findRearrangeButton().exists()).toBe(true);
- });
-
- describe('when rearrange button is clicked', () => {
- const findFirstDraggableRemoveButton = () =>
- findDraggablePanels().at(0).find('.js-draggable-remove');
-
- it('enables draggables', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- expect(findRearrangeButton().attributes('pressed')).toBe('true');
- expect(findEnabledDraggables().wrappers).toEqual(findDraggables().wrappers);
- });
-
- it('metrics can be swapped', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- const firstDraggable = findDraggables().at(0);
- const mockMetrics = [...metricsDashboardViewModel.panelGroups[0].panels];
-
- const firstTitle = mockMetrics[0].title;
- const secondTitle = mockMetrics[1].title;
-
- // swap two elements and `input` them
- [mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]];
- firstDraggable.vm.$emit('input', mockMetrics);
-
- await nextTick();
-
- const { panels } = wrapper.vm.dashboard.panelGroups[0];
-
- expect(panels[1].title).toEqual(firstTitle);
- expect(panels[0].title).toEqual(secondTitle);
- });
-
- it('shows a remove button, which removes a panel', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- expect(findFirstDraggableRemoveButton().find('a').exists()).toBe(true);
-
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount);
- await findFirstDraggableRemoveButton().trigger('click');
-
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1);
- });
-
- it('disables draggables when clicked again', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- findRearrangeButton().vm.$emit('click');
- await nextTick();
- expect(findRearrangeButton().attributes('pressed')).toBeUndefined();
- expect(findEnabledDraggables().length).toBe(0);
- });
- });
- });
- });
-
- describe('cluster health', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true, showHeader: false });
-
- // all_dashboards is not defined in health dashboards
- store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
- await nextTick();
- });
-
- it('hides dashboard header by default', () => {
- expect(wrapper.findComponent({ ref: 'prometheusGraphsHeader' }).exists()).toEqual(false);
- });
-
- it('renders correctly', () => {
- expect(wrapper.html()).not.toBe('');
- });
- });
-
- describe('document title', () => {
- const originalTitle = 'Original Title';
- const overviewDashboardName = dashboardGitResponse[0].display_name;
-
- beforeEach(() => {
- document.title = originalTitle;
- createShallowWrapper({ hasMetrics: true });
- });
-
- afterAll(() => {
- document.title = '';
- });
-
- it('is prepended with the overview dashboard name by default', async () => {
- setupAllDashboards(store);
-
- await nextTick();
- expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
- });
-
- it('is prepended with dashboard name if path is known', async () => {
- const dashboard = dashboardGitResponse[1];
- const currentDashboard = dashboard.path;
-
- setupAllDashboards(store, currentDashboard);
-
- await nextTick();
- expect(document.title.startsWith(`${dashboard.display_name} · `)).toBe(true);
- });
-
- it('is prepended with the overview dashboard name if path is not known', async () => {
- setupAllDashboards(store, 'unknown/path');
-
- await nextTick();
- expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
- });
-
- it('is not modified when dashboard name is not provided', async () => {
- const dashboard = { ...dashboardGitResponse[1], display_name: null };
- const currentDashboard = dashboard.path;
-
- store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, [dashboard]);
-
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard,
- });
-
- await nextTick();
- expect(document.title).toBe(originalTitle);
- });
- });
-
- describe('Clipboard text in panels', () => {
- const currentDashboard = dashboardGitResponse[1].path;
- const panelIndex = 1; // skip expanded panel
-
- const getClipboardTextFirstPanel = () =>
- wrapper.findAllComponents(DashboardPanel).at(panelIndex).props('clipboardText');
-
- beforeEach(async () => {
- setupStoreWithData(store);
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard,
- });
- createShallowWrapper({ hasMetrics: true });
- await nextTick();
- });
-
- it('contains a link to the dashboard', () => {
- const dashboardParam = `dashboard=${encodeURIComponent(currentDashboard)}`;
-
- expect(getClipboardTextFirstPanel()).toContain(dashboardParam);
- expect(getClipboardTextFirstPanel()).toContain(`group=`);
- expect(getClipboardTextFirstPanel()).toContain(`title=`);
- expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
- });
- });
-
- describe('keyboard shortcuts', () => {
- const currentDashboard = dashboardGitResponse[1].path;
- const panelRef = 'dashboard-panel-response-metrics-aws-elb-4-1'; // skip expanded panel
-
- // While the recommendation in the documentation is to test
- // with a data-testid attribute, I want to make sure that
- // the dashboard panels have a ref attribute set.
- const getDashboardPanel = () => wrapper.findComponent({ ref: panelRef });
-
- beforeEach(async () => {
- setupStoreWithData(store);
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard,
- });
- createShallowWrapper({ hasMetrics: true });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hoveredPanel: panelRef });
- await nextTick();
- });
-
- it('contains a ref attribute inside a DashboardPanel component', () => {
- const dashboardPanel = getDashboardPanel();
-
- expect(dashboardPanel.exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
deleted file mode 100644
index 4e220d724f4..00000000000
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import { createStore } from '~/monitoring/stores';
-import { dashboardProps } from '../fixture_data';
-import { setupAllDashboards } from '../store_utils';
-
-jest.mock('~/lib/utils/url_utility');
-
-describe('Dashboard template', () => {
- let wrapper;
- let store;
- let mock;
-
- beforeEach(() => {
- store = createStore({
- currentEnvironmentName: 'production',
- });
- mock = new MockAdapter(axios);
-
- setupAllDashboards(store);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('matches the default snapshot', () => {
- wrapper = shallowMount(Dashboard, {
- propsData: { ...dashboardProps },
- store,
- stubs: {
- DashboardHeader,
- },
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
deleted file mode 100644
index b123d1e7d79..00000000000
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ /dev/null
@@ -1,159 +0,0 @@
-import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import {
- queryToObject,
- redirectTo, // eslint-disable-line import/no-deprecated
- removeParams,
- mergeUrlParams,
- updateHistory,
-} from '~/lib/utils/url_utility';
-
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import { createStore } from '~/monitoring/stores';
-import { defaultTimeRange } from '~/vue_shared/constants';
-import { dashboardProps } from '../fixture_data';
-import { mockProjectDir } from '../mock_data';
-
-jest.mock('~/alert');
-jest.mock('~/lib/utils/url_utility');
-
-describe('dashboard invalid url parameters', () => {
- let store;
- let wrapper;
- let mock;
-
- const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
- wrapper = mount(Dashboard, {
- propsData: { ...dashboardProps, ...props },
- store,
- stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
- ...options,
- });
- };
-
- const findDateTimePicker = () =>
- wrapper.findComponent(DashboardHeader).findComponent({ ref: 'dateTimePicker' });
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch');
-
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- queryToObject.mockReset();
- });
-
- it('passes default url parameters to the time range picker', async () => {
- queryToObject.mockReturnValue({});
-
- createMountedWrapper();
-
- await nextTick();
- expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.any(Object),
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('passes a fixed time range in the URL to the time range picker', async () => {
- const params = {
- start: '2019-01-01T00:00:00.000Z',
- end: '2019-01-10T00:00:00.000Z',
- };
-
- queryToObject.mockReturnValue(params);
-
- createMountedWrapper();
-
- await nextTick();
- expect(findDateTimePicker().props('value')).toEqual(params);
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setTimeRange', params);
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('passes a rolling time range in the URL to the time range picker', async () => {
- queryToObject.mockReturnValue({
- duration_seconds: '120',
- });
-
- createMountedWrapper();
-
- await nextTick();
- const expectedTimeRange = {
- duration: { seconds: 60 * 2 },
- };
-
- expect(findDateTimePicker().props('value')).toMatchObject(expectedTimeRange);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expectedTimeRange,
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('shows an error message and loads a default time range if invalid url parameters are passed', async () => {
- queryToObject.mockReturnValue({
- start: '<script>alert("XSS")</script>',
- end: '<script>alert("XSS")</script>',
- });
-
- createMountedWrapper();
-
- await nextTick();
- expect(createAlert).toHaveBeenCalled();
-
- expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- defaultTimeRange,
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('redirects to different time range', async () => {
- const toUrl = `${mockProjectDir}/-/metrics?environment=1`;
- removeParams.mockReturnValueOnce(toUrl);
-
- createMountedWrapper();
-
- await nextTick();
- findDateTimePicker().vm.$emit('input', {
- duration: { seconds: 120 },
- });
-
- // redirect to with new parameters
- expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
- expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated
- });
-
- it('changes the url when a panel moves the time slider', async () => {
- const timeRange = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- };
-
- queryToObject.mockReturnValue(timeRange);
-
- createMountedWrapper();
-
- await nextTick();
- wrapper.vm.onTimeRangeZoom(timeRange);
-
- expect(updateHistory).toHaveBeenCalled();
- expect(wrapper.vm.selectedTimeRange.start.toString()).toBe(timeRange.start);
- expect(wrapper.vm.selectedTimeRange.end.toString()).toBe(timeRange.end);
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
deleted file mode 100644
index 3ccaa2d28ac..00000000000
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-
-import { dashboardGitResponse } from '../mock_data';
-
-const defaultBranch = 'main';
-const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
-const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
-
-describe('DashboardsDropdown', () => {
- let wrapper;
- let mockDashboards;
- let mockSelectedDashboard;
-
- function createComponent(props, opts = {}) {
- const storeOpts = {
- computed: {
- allDashboards: () => mockDashboards,
- selectedDashboard: () => mockSelectedDashboard,
- },
- };
-
- wrapper = shallowMount(DashboardsDropdown, {
- propsData: {
- ...props,
- defaultBranch,
- },
- ...storeOpts,
- ...opts,
- });
- }
-
- const findItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findItemAt = (i) => wrapper.findAllComponents(GlDropdownItem).at(i);
- const findSearchInput = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownSearch' });
- const findNoItemsMsg = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownMsg' });
- const findStarredListDivider = () => wrapper.findComponent({ ref: 'starredListDivider' });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- const setSearchTerm = (searchTerm) => wrapper.setData({ searchTerm });
-
- beforeEach(() => {
- mockDashboards = dashboardGitResponse;
- mockSelectedDashboard = null;
- });
-
- describe('when it receives dashboards data', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('displays an item for each dashboard', () => {
- expect(findItems().length).toEqual(dashboardGitResponse.length);
- });
-
- it('displays items with the dashboard display name, with starred dashboards first', () => {
- expect(findItemAt(0).text()).toBe(starredDashboards[0].display_name);
- expect(findItemAt(1).text()).toBe(notStarredDashboards[0].display_name);
- expect(findItemAt(2).text()).toBe(notStarredDashboards[1].display_name);
- });
-
- it('displays separator between starred and not starred dashboards', () => {
- expect(findStarredListDivider().exists()).toBe(true);
- });
-
- it('displays a search input', () => {
- expect(findSearchInput().isVisible()).toBe(true);
- });
-
- it('hides no message text by default', () => {
- expect(findNoItemsMsg().isVisible()).toBe(false);
- });
-
- it('filters dropdown items when searched for item exists in the list', async () => {
- const searchTerm = 'Overview';
- setSearchTerm(searchTerm);
- await nextTick();
-
- expect(findItems()).toHaveLength(1);
- });
-
- it('shows no items found message when searched for item does not exists in the list', async () => {
- const searchTerm = 'does-not-exist';
- setSearchTerm(searchTerm);
- await nextTick();
-
- expect(findNoItemsMsg().isVisible()).toBe(true);
- });
- });
-
- describe('when a dashboard is selected', () => {
- beforeEach(() => {
- [mockSelectedDashboard] = starredDashboards;
- createComponent();
- });
-
- it('dashboard item is selected', () => {
- expect(findItemAt(0).props('isChecked')).toBe(true);
- expect(findItemAt(1).props('isChecked')).toBe(false);
- });
- });
-
- describe('when the dashboard is missing a display name', () => {
- beforeEach(() => {
- mockDashboards = dashboardGitResponse.map((d) => ({ ...d, display_name: undefined }));
- createComponent();
- });
-
- it('displays items with the dashboard path, with starred dashboards first', () => {
- expect(findItemAt(0).text()).toBe(starredDashboards[0].path);
- expect(findItemAt(1).text()).toBe(notStarredDashboards[0].path);
- expect(findItemAt(2).text()).toBe(notStarredDashboards[1].path);
- });
- });
-
- describe('when it receives starred dashboards', () => {
- beforeEach(() => {
- mockDashboards = starredDashboards;
- createComponent();
- });
-
- it('displays an item for each dashboard', () => {
- expect(findItems().length).toEqual(starredDashboards.length);
- });
-
- it('displays a star icon', () => {
- const star = findItemAt(0).findComponent(GlIcon);
- expect(star.exists()).toBe(true);
- expect(star.attributes('name')).toBe('star');
- });
-
- it('displays no separator between starred and not starred dashboards', () => {
- expect(findStarredListDivider().exists()).toBe(false);
- });
- });
-
- describe('when it receives only not-starred dashboards', () => {
- beforeEach(() => {
- mockDashboards = notStarredDashboards;
- createComponent();
- });
-
- it('displays an item for each dashboard', () => {
- expect(findItems().length).toEqual(notStarredDashboards.length);
- });
-
- it('displays no star icon', () => {
- const star = findItemAt(0).findComponent(GlIcon);
- expect(star.exists()).toBe(false);
- });
-
- it('displays no separator between starred and not starred dashboards', () => {
- expect(findStarredListDivider().exists()).toBe(false);
- });
- });
-
- describe('when a dashboard gets selected by the user', () => {
- beforeEach(() => {
- createComponent();
- findItemAt(1).vm.$emit('click');
- });
-
- it('emits a "selectDashboard" event with dashboard information', () => {
- expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[0]]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
deleted file mode 100644
index b54ca926dae..00000000000
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
-
-import { dashboardGitResponse } from '../mock_data';
-
-let wrapper;
-
-const createMountedWrapper = (props = {}) => {
- // Use `mount` to render native input elements
- wrapper = mount(DuplicateDashboardForm, {
- propsData: { ...props },
- // We need to attach to document, so that `document.activeElement` is properly set in jsdom
- attachTo: document.body,
- });
-};
-
-describe('DuplicateDashboardForm', () => {
- const defaultBranch = 'main';
-
- const findByRef = (ref) => wrapper.findComponent({ ref });
- const setValue = (ref, val) => {
- findByRef(ref).setValue(val);
- };
- const setChecked = (value) => {
- const input = wrapper.find(`.custom-control-input[value="${value}"]`);
- input.element.checked = true;
- input.trigger('click');
- input.trigger('change');
- };
-
- beforeEach(() => {
- createMountedWrapper({ dashboard: dashboardGitResponse[0], defaultBranch });
- });
-
- it('renders correctly', () => {
- expect(wrapper.exists()).toEqual(true);
- });
-
- it('renders form elements', () => {
- expect(findByRef('fileName').exists()).toEqual(true);
- expect(findByRef('branchName').exists()).toEqual(true);
- expect(findByRef('branchOption').exists()).toEqual(true);
- expect(findByRef('commitMessage').exists()).toEqual(true);
- });
-
- describe('validates the file name', () => {
- const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback');
-
- it('when is empty', async () => {
- setValue('fileName', '');
- await nextTick();
-
- expect(findByRef('fileNameFormGroup').classes()).toContain('is-valid');
- expect(findInvalidFeedback().exists()).toBe(false);
- });
-
- it('when is valid', async () => {
- setValue('fileName', 'my_dashboard.yml');
- await nextTick();
-
- expect(findByRef('fileNameFormGroup').classes()).toContain('is-valid');
- expect(findInvalidFeedback().exists()).toBe(false);
- });
-
- it('when is not valid', async () => {
- setValue('fileName', 'my_dashboard.exe');
- await nextTick();
-
- expect(findByRef('fileNameFormGroup').classes()).toContain('is-invalid');
- expect(findInvalidFeedback().text()).toBe('The file name should have a .yml extension');
- });
- });
-
- describe('emits `change` event', () => {
- const lastChange = () =>
- nextTick().then(() => {
- wrapper.find('form').trigger('change');
-
- // Resolves to the last emitted change
- const changes = wrapper.emitted().change;
- return changes[changes.length - 1][0];
- });
-
- it('with the inital form values', () => {
- expect(wrapper.emitted().change).toHaveLength(1);
-
- return expect(lastChange()).resolves.toEqual({
- branch: '',
- commitMessage: expect.any(String),
- dashboard: dashboardGitResponse[0].path,
- fileName: 'common_metrics.yml',
- });
- });
-
- it('containing an inputted file name', () => {
- setValue('fileName', 'my_dashboard.yml');
-
- return expect(lastChange()).resolves.toMatchObject({
- fileName: 'my_dashboard.yml',
- });
- });
-
- it('containing a default commit message when no message is set', () => {
- setValue('commitMessage', '');
-
- return expect(lastChange()).resolves.toMatchObject({
- commitMessage: expect.stringContaining('Create custom dashboard'),
- });
- });
-
- it('containing an inputted commit message', () => {
- setValue('commitMessage', 'My commit message');
-
- return expect(lastChange()).resolves.toMatchObject({
- commitMessage: expect.stringContaining('My commit message'),
- });
- });
-
- it('containing an inputted branch name', () => {
- setValue('branchName', 'a-new-branch');
-
- return expect(lastChange()).resolves.toMatchObject({
- branch: 'a-new-branch',
- });
- });
-
- it('when a `default` branch option is set, branch input is invisible and ignored', () => {
- setChecked(wrapper.vm.$options.radioVals.DEFAULT);
- setValue('branchName', 'a-new-branch');
-
- return Promise.all([
- expect(lastChange()).resolves.toMatchObject({
- branch: defaultBranch,
- }),
- nextTick(() => {
- expect(findByRef('branchName').isVisible()).toBe(false);
- }),
- ]);
- });
-
- it('when `new` branch option is chosen, focuses on the branch name input', async () => {
- setChecked(wrapper.vm.$options.radioVals.NEW);
-
- await nextTick();
-
- wrapper.find('form').trigger('change');
- expect(document.activeElement).toBe(findByRef('branchName').element);
- });
- });
-});
-
-describe('DuplicateDashboardForm escapes elements', () => {
- const branchToEscape = "<img/src='x'onerror=alert(document.domain)>";
-
- beforeEach(() => {
- createMountedWrapper({ dashboard: dashboardGitResponse[0], defaultBranch: branchToEscape });
- });
-
- it('should escape branch name data', () => {
- const branchOptionHtml = wrapper.vm.branchOptions[0].html;
- const escapedBranch = '&lt;img/src=&#39;x&#39;onerror=alert(document.domain)&gt';
-
- expect(branchOptionHtml).toEqual(expect.stringContaining(escapedBranch));
- });
-});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
deleted file mode 100644
index d83a9192876..00000000000
--- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-
-import waitForPromises from 'helpers/wait_for_promises';
-
-import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
-import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
-
-import { dashboardGitResponse } from '../mock_data';
-
-Vue.use(Vuex);
-
-describe('duplicate dashboard modal', () => {
- let wrapper;
- let mockDashboards;
- let mockSelectedDashboard;
- let duplicateDashboardAction;
- let okEvent;
-
- function createComponent() {
- const store = new Vuex.Store({
- modules: {
- monitoringDashboard: {
- namespaced: true,
- actions: {
- duplicateSystemDashboard: duplicateDashboardAction,
- },
- getters: {
- allDashboards: () => mockDashboards,
- selectedDashboard: () => mockSelectedDashboard,
- },
- },
- },
- });
-
- return shallowMount(DuplicateDashboardModal, {
- propsData: {
- defaultBranch: 'main',
- modalId: 'id',
- },
- store,
- });
- }
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findModal = () => wrapper.findComponent(GlModal);
- const findDuplicateDashboardForm = () => wrapper.findComponent(DuplicateDashboardForm);
-
- beforeEach(() => {
- mockDashboards = dashboardGitResponse;
- [mockSelectedDashboard] = dashboardGitResponse;
-
- duplicateDashboardAction = jest.fn().mockResolvedValue();
-
- okEvent = {
- preventDefault: jest.fn(),
- };
-
- wrapper = createComponent();
-
- wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
- });
-
- it('contains a form to duplicate a dashboard', () => {
- expect(findDuplicateDashboardForm().exists()).toBe(true);
- });
-
- it('saves a new dashboard', async () => {
- findModal().vm.$emit('ok', okEvent);
-
- await waitForPromises();
- expect(okEvent.preventDefault).toHaveBeenCalled();
- expect(wrapper.emitted('dashboardDuplicated')).toHaveLength(1);
- expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]);
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
- expect(findAlert().exists()).toBe(false);
- });
-
- it('handles error when a new dashboard is not saved', async () => {
- const errMsg = 'An error occurred';
-
- duplicateDashboardAction.mockRejectedValueOnce(errMsg);
- findModal().vm.$emit('ok', okEvent);
-
- await waitForPromises();
-
- expect(okEvent.preventDefault).toHaveBeenCalled();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errMsg);
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
- });
-
- it('updates the form on changes', () => {
- const formVals = {
- dashboard: 'common_metrics.yml',
- commitMessage: 'A commit message',
- };
-
- findModal().findComponent(DuplicateDashboardForm).vm.$emit('change', formVals);
-
- // Binding's second argument contains the modal id
- expect(wrapper.vm.form).toEqual(formVals);
- });
-});
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
deleted file mode 100644
index beb698c838f..00000000000
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import { GlButton, GlCard } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { TEST_HOST } from 'helpers/test_constants';
-import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
-import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
-import {
- addModuleAction,
- initialEmbedGroupState,
- singleEmbedProps,
- dashboardEmbedProps,
- multipleEmbedProps,
-} from './mock_data';
-
-Vue.use(Vuex);
-
-describe('Embed Group', () => {
- let wrapper;
- let store;
- const metricsWithDataGetter = jest.fn();
-
- function mountComponent({ urls = [TEST_HOST], shallow = true, stubs } = {}) {
- const mountMethod = shallow ? shallowMount : mount;
- wrapper = mountMethod(EmbedGroup, {
- store,
- propsData: {
- urls,
- },
- stubs,
- });
- }
-
- beforeEach(() => {
- store = new Vuex.Store({
- modules: {
- embedGroup: {
- namespaced: true,
- actions: { addModule: jest.fn() },
- getters: { metricsWithData: metricsWithDataGetter },
- state: initialEmbedGroupState,
- },
- },
- });
- store.registerModule = jest.fn();
- jest.spyOn(store, 'dispatch');
- });
-
- afterEach(() => {
- metricsWithDataGetter.mockReset();
- });
-
- describe('interactivity', () => {
- it('hides the component when no chart data is loaded', () => {
- metricsWithDataGetter.mockReturnValue([]);
- mountComponent();
-
- expect(wrapper.findComponent(GlCard).isVisible()).toBe(false);
- });
-
- it('shows the component when chart data is loaded', () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent();
-
- expect(wrapper.findComponent(GlCard).isVisible()).toBe(true);
- });
-
- it('is expanded by default', () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- expect(wrapper.find('.gl-card-body').classes()).not.toContain('d-none');
- });
-
- it('collapses when clicked', async () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- wrapper.findComponent(GlButton).trigger('click');
-
- await nextTick();
- expect(wrapper.find('.gl-card-body').classes()).toContain('d-none');
- });
- });
-
- describe('single metrics', () => {
- beforeEach(() => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent();
- });
-
- it('renders an Embed component', () => {
- expect(wrapper.findComponent(MetricEmbed).exists()).toBe(true);
- });
-
- it('passes the correct props to the Embed component', () => {
- expect(wrapper.findComponent(MetricEmbed).props()).toEqual(singleEmbedProps());
- });
-
- it('adds the monitoring dashboard module', () => {
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
- });
- });
-
- describe('dashboard metrics', () => {
- beforeEach(() => {
- metricsWithDataGetter.mockReturnValue([2]);
- mountComponent();
- });
-
- it('passes the correct props to the dashboard Embed component', () => {
- expect(wrapper.findComponent(MetricEmbed).props()).toEqual(dashboardEmbedProps());
- });
-
- it('adds the monitoring dashboard module', () => {
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
- });
- });
-
- describe('multiple metrics', () => {
- beforeEach(() => {
- metricsWithDataGetter.mockReturnValue([1, 1]);
- mountComponent({ urls: [TEST_HOST, TEST_HOST] });
- });
-
- it('creates Embed components', () => {
- expect(wrapper.findAllComponents(MetricEmbed)).toHaveLength(2);
- });
-
- it('passes the correct props to the Embed components', () => {
- expect(wrapper.findAllComponents(MetricEmbed).wrappers.map((item) => item.props())).toEqual(
- multipleEmbedProps(),
- );
- });
-
- it('adds multiple monitoring dashboard modules', () => {
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/1');
- });
- });
-
- describe('button text', () => {
- it('has a singular label when there is one embed', () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- expect(wrapper.findComponent(GlButton).text()).toBe('Hide chart');
- });
-
- it('has a plural label when there are multiple embeds', () => {
- metricsWithDataGetter.mockReturnValue([2]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- expect(wrapper.findComponent(GlButton).text()).toBe('Hide charts');
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
deleted file mode 100644
index db25d524592..00000000000
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { setHTMLFixture } from 'helpers/fixtures';
-import { TEST_HOST } from 'helpers/test_constants';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
-import { groups, initialState, metricsData, metricsWithData } from './mock_data';
-
-Vue.use(Vuex);
-
-describe('MetricEmbed', () => {
- let wrapper;
- let store;
- let actions;
- let metricsWithDataGetter;
-
- function mountComponent() {
- wrapper = shallowMount(MetricEmbed, {
- store,
- propsData: {
- dashboardUrl: TEST_HOST,
- },
- });
- }
-
- beforeEach(() => {
- setHTMLFixture('<div class="layout-page"></div>');
-
- actions = {
- setInitialState: jest.fn(),
- setShowErrorBanner: jest.fn(),
- setTimeRange: jest.fn(),
- fetchDashboard: jest.fn(),
- };
-
- metricsWithDataGetter = jest.fn();
-
- store = new Vuex.Store({
- modules: {
- monitoringDashboard: {
- namespaced: true,
- actions,
- getters: {
- metricsWithData: () => metricsWithDataGetter,
- },
- state: initialState,
- },
- },
- });
- });
-
- afterEach(() => {
- metricsWithDataGetter.mockClear();
- });
-
- describe('no metrics are available yet', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('shows an empty state when no metrics are present', () => {
- expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPanel).exists()).toBe(false);
- });
- });
-
- describe('metrics are available', () => {
- beforeEach(() => {
- store.state.monitoringDashboard.dashboard.panelGroups = groups;
- store.state.monitoringDashboard.dashboard.panelGroups[0].panels = metricsData;
-
- metricsWithDataGetter.mockReturnValue(metricsWithData);
-
- mountComponent();
- });
-
- it('calls actions to fetch data', () => {
- const expectedTimeRangePayload = expect.objectContaining({
- start: expect.any(String),
- end: expect.any(String),
- });
-
- expect(actions.setTimeRange).toHaveBeenCalledTimes(1);
- expect(actions.setTimeRange.mock.calls[0][1]).toEqual(expectedTimeRangePayload);
-
- expect(actions.fetchDashboard).toHaveBeenCalled();
- });
-
- it('shows a chart when metrics are present', () => {
- expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPanel).exists()).toBe(true);
- expect(wrapper.findAllComponents(DashboardPanel).length).toBe(2);
- });
-
- it('includes groupId with dashboardUrl', () => {
- expect(wrapper.findComponent(DashboardPanel).props('groupId')).toBe(TEST_HOST);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/embeds/mock_data.js b/spec/frontend/monitoring/components/embeds/mock_data.js
deleted file mode 100644
index e32e1a08cdb..00000000000
--- a/spec/frontend/monitoring/components/embeds/mock_data.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { TEST_HOST } from 'helpers/test_constants';
-
-export const metricsWithData = ['15_metric_a', '16_metric_b'];
-
-export const groups = [
- {
- panels: [
- {
- title: 'Memory Usage (Total)',
- type: 'area-chart',
- y_label: 'Total Memory Used',
- metrics: null,
- },
- ],
- },
-];
-
-const result = [
- {
- values: [
- ['Mon', 1220],
- ['Tue', 932],
- ['Wed', 901],
- ['Thu', 934],
- ['Fri', 1290],
- ['Sat', 1330],
- ['Sun', 1320],
- ],
- },
-];
-
-export const metricsData = [
- {
- metrics: [
- {
- metricId: '15_metric_a',
- result,
- },
- ],
- },
- {
- metrics: [
- {
- metricId: '16_metric_b',
- result,
- },
- ],
- },
-];
-
-export const initialState = () => ({
- dashboard: {
- panel_groups: [],
- },
-});
-
-export const initialEmbedGroupState = () => ({
- modules: [],
-});
-
-export const singleEmbedProps = () => ({
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-12',
- namespace: 'monitoringDashboard/0',
-});
-
-export const dashboardEmbedProps = () => ({
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-6',
- namespace: 'monitoringDashboard/0',
-});
-
-export const multipleEmbedProps = () => [
- {
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-6',
- namespace: 'monitoringDashboard/0',
- },
- {
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-6',
- namespace: 'monitoringDashboard/1',
- },
-];
-
-export const addModuleAction = 'embedGroup/addModule';
diff --git a/spec/frontend/monitoring/components/empty_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js
deleted file mode 100644
index ddefa8c5cd0..00000000000
--- a/spec/frontend/monitoring/components/empty_state_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import EmptyState from '~/monitoring/components/empty_state.vue';
-import { dashboardEmptyStates } from '~/monitoring/constants';
-
-function createComponent(props) {
- return shallowMount(EmptyState, {
- propsData: {
- settingsPath: '/settingsPath',
- clustersPath: '/clustersPath',
- documentationPath: '/documentationPath',
- emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
- emptyLoadingSvgPath: '/path/to/loading.svg',
- emptyNoDataSvgPath: '/path/to/no-data.svg',
- emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
- emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
- ...props,
- },
- });
-}
-
-describe('EmptyState', () => {
- it('shows loading state with a loading icon', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.LOADING,
- });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
- });
-
- it('shows gettingStarted state', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.GETTING_STARTED,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('shows unableToConnect state', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.UNABLE_TO_CONNECT,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('shows noData state', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.NO_DATA,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
deleted file mode 100644
index 593d832f297..00000000000
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import GraphGroup from '~/monitoring/components/graph_group.vue';
-
-describe('Graph group component', () => {
- let wrapper;
-
- const findGroup = () => wrapper.findComponent({ ref: 'graph-group' });
- const findContent = () => wrapper.findComponent({ ref: 'graph-group-content' });
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCaretIcon = () => wrapper.findComponent(GlIcon);
- const findToggleButton = () => wrapper.find('[data-testid="group-toggle-button"]');
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(GraphGroup, {
- propsData,
- });
- };
-
- describe('When group is not collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- collapseGroup: false,
- });
- });
-
- it('should not show a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('should show the chevron-lg-down caret icon', () => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
- });
-
- it('should show the chevron-lg-right caret icon when the user collapses the group', async () => {
- findToggleButton().trigger('click');
-
- await nextTick();
- expect(findContent().isVisible()).toBe(false);
- expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
- });
-
- it('should contain a tab index for the collapse button', () => {
- const groupToggle = findToggleButton();
-
- expect(groupToggle.attributes('tabindex')).toBeDefined();
- });
-
- it('should show the open the group when collapseGroup is set to true', async () => {
- wrapper.setProps({
- collapseGroup: true,
- });
-
- await nextTick();
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
- });
- });
-
- describe('When group is collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- collapseGroup: true,
- });
- });
-
- it('should show the chevron-lg-down caret icon when collapseGroup is true', () => {
- expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
- });
-
- it('should show the chevron-lg-right caret icon when collapseGroup is false', async () => {
- findToggleButton().trigger('click');
-
- await nextTick();
- expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
- });
-
- it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
- const graphGroupContent = findContent();
- const button = findToggleButton();
-
- button.trigger('keyup.enter');
-
- expect(graphGroupContent.isVisible()).toBe(false);
- });
- });
-
- describe('When groups can not be collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- showPanels: false,
- collapseGroup: false,
- });
- });
-
- it('should not have a container when showPanels is false', () => {
- expect(findGroup().exists()).toBe(false);
- expect(findContent().exists()).toBe(true);
- });
- });
-
- describe('When group is loading', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- isLoading: true,
- });
- });
-
- it('should show a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
- });
-
- describe('When group does not show a panel heading', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- showPanels: false,
- collapseGroup: false,
- });
- });
-
- it('should collapse the panel content', () => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().exists()).toBe(false);
- });
-
- it('should show the panel content when collapse is set to false', async () => {
- wrapper.setProps({
- collapseGroup: false,
- });
-
- await nextTick();
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
deleted file mode 100644
index d3a48be7939..00000000000
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { stubComponent } from 'helpers/stub_component';
-import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
-import { metricStates } from '~/monitoring/constants';
-
-function createComponent(props) {
- return shallowMount(GroupEmptyState, {
- propsData: {
- ...props,
- documentationPath: '/path/to/docs',
- settingsPath: '/path/to/settings',
- svgPath: '/path/to/empty-group-illustration.svg',
- },
- stubs: {
- GlEmptyState: stubComponent(GlEmptyState, {
- template: '<div><slot name="description"></slot></div>',
- }),
- },
- });
-}
-
-describe('GroupEmptyState', () => {
- let wrapper;
-
- describe.each([
- metricStates.NO_DATA,
- metricStates.TIMEOUT,
- metricStates.CONNECTION_FAILED,
- metricStates.BAD_QUERY,
- metricStates.LOADING,
- metricStates.UNKNOWN_ERROR,
- 'FOO STATE', // does not fail with unknown states
- ])('given state %s', (selectedState) => {
- beforeEach(() => {
- wrapper = createComponent({ selectedState });
- });
-
- it('renders the slotted content', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('passes the expected props to GlEmptyState', () => {
- expect(wrapper.findComponent(GlEmptyState).props()).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
deleted file mode 100644
index 94938e7f459..00000000000
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-
-import LinksSection from '~/monitoring/components/links_section.vue';
-import { createStore } from '~/monitoring/stores';
-
-describe('Links Section component', () => {
- let store;
- let wrapper;
-
- const createShallowWrapper = () => {
- wrapper = shallowMount(LinksSection, {
- store,
- });
- };
- const setState = (links) => {
- store.state.monitoringDashboard = {
- ...store.state.monitoringDashboard,
- emptyState: null,
- links,
- };
- };
- const findLinks = () => wrapper.findAllComponents(GlLink);
-
- beforeEach(() => {
- store = createStore();
- createShallowWrapper();
- });
-
- it('does not render a section if no links are present', async () => {
- setState();
-
- await nextTick();
-
- expect(findLinks().length).toBe(0);
- });
-
- it('renders a link inside a section', async () => {
- setState([
- {
- title: 'GitLab Website',
- url: 'https://gitlab.com',
- },
- ]);
-
- await nextTick();
- expect(findLinks()).toHaveLength(1);
- const firstLink = findLinks().at(0);
-
- expect(firstLink.attributes('href')).toBe('https://gitlab.com');
- expect(firstLink.text()).toBe('GitLab Website');
- });
-
- it('renders multiple links inside a section', async () => {
- const links = new Array(10)
- .fill(null)
- .map((_, i) => ({ title: `Title ${i}`, url: `https://gitlab.com/projects/${i}` }));
- setState(links);
-
- await nextTick();
- expect(findLinks()).toHaveLength(10);
- });
-});
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
deleted file mode 100644
index f6cc6789b1f..00000000000
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Visibility from 'visibilityjs';
-import { nextTick } from 'vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import { createStore } from '~/monitoring/stores';
-
-describe('RefreshButton', () => {
- let wrapper;
- let store;
- let dispatch;
- let documentHidden;
-
- const createWrapper = (options = {}) => {
- wrapper = shallowMount(RefreshButton, { store, ...options });
- };
-
- const findRefreshBtn = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findOptions = () => findDropdown().findAllComponents(GlDropdownItem);
- const findOptionAt = (index) => findOptions().at(index);
-
- const expectFetchDataToHaveBeenCalledTimes = (times) => {
- const refreshCalls = dispatch.mock.calls.filter(([action, payload]) => {
- return action === 'monitoringDashboard/fetchDashboardData' && payload === undefined;
- });
- expect(refreshCalls).toHaveLength(times);
- };
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- dispatch = store.dispatch;
-
- documentHidden = false;
- jest.spyOn(Visibility, 'hidden').mockImplementation(() => documentHidden);
-
- createWrapper();
- });
-
- afterEach(() => {
- dispatch.mockReset();
- // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
- wrapper.destroy();
- });
-
- it('refreshes data when "refresh" is clicked', () => {
- findRefreshBtn().vm.$emit('click');
- expectFetchDataToHaveBeenCalledTimes(1);
- });
-
- it('refresh rate is "Off" in the dropdown', () => {
- expect(findDropdown().props('text')).toBe('Off');
- });
-
- describe('refresh rate options', () => {
- it('presents multiple options', () => {
- expect(findOptions().length).toBeGreaterThan(1);
- });
-
- it('presents an "Off" option as the default option', () => {
- expect(findOptionAt(0).text()).toBe('Off');
- expect(findOptionAt(0).props('isChecked')).toBe(true);
- });
- });
-
- describe('when a refresh rate is chosen', () => {
- const optIndex = 2; // Other option than "Off"
-
- beforeEach(async () => {
- findOptionAt(optIndex).vm.$emit('click');
- await nextTick();
- });
-
- it('refresh rate appears in the dropdown', () => {
- expect(findDropdown().props('text')).toBe('10s');
- });
-
- it('refresh rate option is checked', () => {
- expect(findOptionAt(0).props('isChecked')).toBe(false);
- expect(findOptionAt(optIndex).props('isChecked')).toBe(true);
- });
-
- it('refreshes data when a new refresh rate is chosen', () => {
- expectFetchDataToHaveBeenCalledTimes(1);
- });
-
- it('refreshes data after two intervals of time have passed', async () => {
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(2);
-
- await nextTick();
-
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(3);
- });
-
- it('does not refresh data if the document is hidden', async () => {
- documentHidden = true;
-
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(1);
-
- await nextTick();
-
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(1);
- });
-
- it('data is not refreshed anymore after component is destroyed', () => {
- expect(jest.getTimerCount()).toBe(1);
-
- wrapper.destroy();
-
- expect(jest.getTimerCount()).toBe(0);
- });
-
- describe('when "Off" refresh rate is chosen', () => {
- beforeEach(async () => {
- findOptionAt(0).vm.$emit('click');
- await nextTick();
- });
-
- it('refresh rate is "Off" in the dropdown', () => {
- expect(findDropdown().props('text')).toBe('Off');
- });
-
- it('refresh rate option is appears selected', () => {
- expect(findOptionAt(0).props('isChecked')).toBe(true);
- expect(findOptionAt(optIndex).props('isChecked')).toBe(false);
- });
-
- it('stops refreshing data', () => {
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(1);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
deleted file mode 100644
index e6c5569fa19..00000000000
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
-
-describe('Custom variable component', () => {
- let wrapper;
-
- const defaultProps = {
- name: 'env',
- label: 'Select environment',
- value: 'Production',
- options: {
- values: [
- { text: 'Production', value: 'prod' },
- { text: 'Canary', value: 'canary' },
- ],
- },
- };
-
- const createShallowWrapper = (props) => {
- wrapper = shallowMount(DropdownField, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
-
- it('renders dropdown element when all necessary props are passed', () => {
- createShallowWrapper();
-
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('renders dropdown element with a text', () => {
- createShallowWrapper();
-
- expect(findDropdown().attributes('text')).toBe(defaultProps.value);
- });
-
- it('renders all the dropdown items', () => {
- createShallowWrapper();
-
- expect(findDropdownItems()).toHaveLength(defaultProps.options.values.length);
- });
-
- it('renders dropdown when values are missing', () => {
- createShallowWrapper({ options: {} });
-
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('changing dropdown items triggers update', () => {
- createShallowWrapper();
- findDropdownItems().at(1).vm.$emit('click');
-
- 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
deleted file mode 100644
index 20e1937c5ac..00000000000
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { GlFormInput } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import TextField from '~/monitoring/components/variables/text_field.vue';
-
-describe('Text variable component', () => {
- let wrapper;
- const propsData = {
- name: 'pod',
- label: 'Select pod',
- value: 'test-pod',
- };
- const createShallowWrapper = () => {
- wrapper = shallowMount(TextField, {
- propsData,
- });
- };
-
- const findInput = () => wrapper.findComponent(GlFormInput);
-
- it('renders a text input when all props are passed', () => {
- createShallowWrapper();
-
- expect(findInput().exists()).toBe(true);
- });
-
- it('always has a default value', async () => {
- createShallowWrapper();
-
- await nextTick();
- expect(findInput().attributes('value')).toBe(propsData.value);
- });
-
- it('triggers keyup enter', async () => {
- createShallowWrapper();
-
- findInput().element.value = 'prod-pod';
- findInput().trigger('input');
- findInput().trigger('keyup.enter');
-
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['prod-pod']]);
- });
-
- it('triggers blur enter', async () => {
- createShallowWrapper();
-
- findInput().element.value = 'canary-pod';
- findInput().trigger('input');
- findInput().trigger('blur');
-
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['canary-pod']]);
- });
-});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
deleted file mode 100644
index d6f8aac99aa..00000000000
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
-import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
-import TextField from '~/monitoring/components/variables/text_field.vue';
-import VariablesSection from '~/monitoring/components/variables_section.vue';
-import { createStore } from '~/monitoring/stores';
-import { convertVariablesForURL } from '~/monitoring/utils';
-import { storeVariables } from '../mock_data';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- updateHistory: jest.fn(),
- mergeUrlParams: jest.fn(),
-}));
-
-describe('Metrics dashboard/variables section component', () => {
- let store;
- let wrapper;
-
- const createShallowWrapper = () => {
- wrapper = shallowMount(VariablesSection, {
- store,
- });
- };
-
- const findTextInputs = () => wrapper.findAllComponents(TextField);
- const findCustomInputs = () => wrapper.findAllComponents(DropdownField);
-
- beforeEach(() => {
- store = createStore();
-
- store.state.monitoringDashboard.emptyState = null;
- });
-
- it('does not show the variables section', () => {
- createShallowWrapper();
- const allInputs = findTextInputs().length + findCustomInputs().length;
-
- expect(allInputs).toBe(0);
- });
-
- describe('when variables are set', () => {
- beforeEach(async () => {
- store.state.monitoringDashboard.variables = storeVariables;
- createShallowWrapper();
-
- await nextTick();
- });
-
- it('shows the variables section', () => {
- const allInputs = findTextInputs().length + findCustomInputs().length;
-
- expect(allInputs).toBe(storeVariables.length);
- });
-
- it('shows the right custom variable inputs', () => {
- const customInputs = findCustomInputs();
-
- expect(customInputs.at(0).props('name')).toBe('customSimple');
- expect(customInputs.at(1).props('name')).toBe('customAdvanced');
- });
- });
-
- describe('when changing the variable inputs', () => {
- const updateVariablesAndFetchData = jest.fn();
-
- beforeEach(() => {
- store = new Vuex.Store({
- modules: {
- monitoringDashboard: {
- namespaced: true,
- state: {
- emptyState: null,
- variables: storeVariables,
- },
- actions: {
- updateVariablesAndFetchData,
- },
- },
- },
- });
-
- createShallowWrapper();
- });
-
- it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', async () => {
- const firstInput = findTextInputs().at(0);
-
- firstInput.vm.$emit('input', 'test');
-
- await nextTick();
- expect(updateVariablesAndFetchData).toHaveBeenCalled();
- expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(storeVariables),
- window.location.href,
- );
- expect(updateHistory).toHaveBeenCalled();
- });
-
- it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', async () => {
- const firstInput = findCustomInputs().at(0);
-
- firstInput.vm.$emit('input', 'test');
-
- await nextTick();
- expect(updateVariablesAndFetchData).toHaveBeenCalled();
- expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(storeVariables),
- window.location.href,
- );
- expect(updateHistory).toHaveBeenCalled();
- });
-
- it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
- const firstInput = findTextInputs().at(0);
-
- firstInput.vm.$emit('input', 'My default value');
-
- expect(updateVariablesAndFetchData).not.toHaveBeenCalled();
- expect(mergeUrlParams).not.toHaveBeenCalled();
- expect(updateHistory).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js
deleted file mode 100644
index 42d19c21a7b..00000000000
--- a/spec/frontend/monitoring/csv_export_spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { graphDataToCsv } from '~/monitoring/csv_export';
-import { timeSeriesGraphData } from './graph_data';
-
-describe('monitoring export_csv', () => {
- describe('graphDataToCsv', () => {
- const expectCsvToMatchLines = (csv, lines) => expect(`${lines.join('\r\n')}\r\n`).toEqual(csv);
-
- it('should return a csv with 0 metrics', () => {
- const data = timeSeriesGraphData({}, { metricCount: 0 });
-
- expect(graphDataToCsv(data)).toEqual('');
- });
-
- it('should return a csv with 1 metric with no data', () => {
- const data = timeSeriesGraphData({}, { metricCount: 1 });
-
- // When state is NO_DATA, result is null
- data.metrics[0].result = null;
-
- expect(graphDataToCsv(data)).toEqual('');
- });
-
- it('should return a csv with 1 metric', () => {
- const data = timeSeriesGraphData({}, { metricCount: 1 });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1"`,
- '2015-07-01T20:10:50.000Z,1',
- '2015-07-01T20:12:50.000Z,2',
- '2015-07-01T20:14:50.000Z,3',
- ]);
- });
-
- it('should return a csv with multiple metrics and one with no data', () => {
- const data = timeSeriesGraphData({}, { metricCount: 2 });
-
- // When state is NO_DATA, result is null
- data.metrics[0].result = null;
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 2"`,
- '2015-07-01T20:10:50.000Z,1',
- '2015-07-01T20:12:50.000Z,2',
- '2015-07-01T20:14:50.000Z,3',
- ]);
- });
-
- it('should return a csv when not all metrics have the same timestamps', () => {
- const data = timeSeriesGraphData({}, { metricCount: 3 });
-
- // Add an "odd" timestamp that is not in the dataset
- Object.assign(data.metrics[2].result[0], {
- value: ['2016-01-01T00:00:00.000Z', 9],
- values: [['2016-01-01T00:00:00.000Z', 9]],
- });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
- '2015-07-01T20:10:50.000Z,1,1,',
- '2015-07-01T20:12:50.000Z,2,2,',
- '2015-07-01T20:14:50.000Z,3,3,',
- '2016-01-01T00:00:00.000Z,,,9',
- ]);
- });
-
- it('should escape double quotes in metric labels with two double quotes ("")', () => {
- const data = timeSeriesGraphData({}, { metricCount: 1 });
-
- data.metrics[0].label = 'My "quoted" metric';
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > My ""quoted"" metric"`,
- '2015-07-01T20:10:50.000Z,1',
- '2015-07-01T20:12:50.000Z,2',
- '2015-07-01T20:14:50.000Z,3',
- ]);
- });
-
- it('should return a csv with multiple metrics', () => {
- const data = timeSeriesGraphData({}, { metricCount: 3 });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
- '2015-07-01T20:10:50.000Z,1,1,1',
- '2015-07-01T20:12:50.000Z,2,2,2',
- '2015-07-01T20:14:50.000Z,3,3,3',
- ]);
- });
-
- it('should return a csv with 1 metric and multiple series with labels', () => {
- const data = timeSeriesGraphData({}, { isMultiSeries: true });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1","Y Axis > Metric 1"`,
- '2015-07-01T20:10:50.000Z,1,4',
- '2015-07-01T20:12:50.000Z,2,5',
- '2015-07-01T20:14:50.000Z,3,6',
- ]);
- });
-
- it('should return a csv with 1 metric and multiple series', () => {
- const data = timeSeriesGraphData({}, { isMultiSeries: true, withLabels: false });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
- '2015-07-01T20:10:50.000Z,1,4',
- '2015-07-01T20:12:50.000Z,2,5',
- '2015-07-01T20:14:50.000Z,3,6',
- ]);
- });
-
- it('should return a csv with multiple metrics and multiple series', () => {
- const data = timeSeriesGraphData(
- {},
- { metricCount: 3, isMultiSeries: true, withLabels: false },
- );
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
- '2015-07-01T20:10:50.000Z,1,4,1,4,1,4',
- '2015-07-01T20:12:50.000Z,2,5,2,5,2,5',
- '2015-07-01T20:14:50.000Z,3,6,3,6,3,6',
- ]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
deleted file mode 100644
index f4062adea81..00000000000
--- a/spec/frontend/monitoring/fixture_data.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import fixture from 'test_fixtures/metrics_dashboard/environment_metrics_dashboard.json';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { metricStates } from '~/monitoring/constants';
-import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
-import { stateAndPropsFromDataset } from '~/monitoring/utils';
-
-import { metricsResult } from './mock_data';
-
-export const metricsDashboardResponse = fixture;
-
-export const metricsDashboardPayload = metricsDashboardResponse.dashboard;
-
-const datasetState = stateAndPropsFromDataset(
- convertObjectPropsToCamelCase(metricsDashboardResponse.metrics_data),
-);
-
-// new properties like addDashboardDocumentationPath prop
-// was recently added to dashboard.vue component this needs to be
-// added to fixtures data
-// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
-export const dashboardProps = {
- ...datasetState.dataProps,
-};
-
-export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload);
-
-export const metricsDashboardPanelCount = 22;
-
-// Graph data
-
-const firstPanel = metricsDashboardViewModel.panelGroups[0].panels[0];
-
-export const graphData = {
- ...firstPanel,
- metrics: firstPanel.metrics.map((metric) => ({
- ...metric,
- result: metricsResult,
- state: metricStates.OK,
- })),
-};
-
-export const graphDataEmpty = {
- ...firstPanel,
- metrics: firstPanel.metrics.map((metric) => ({
- ...metric,
- result: [],
- state: metricStates.NO_DATA,
- })),
-};
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
deleted file mode 100644
index 981955efebb..00000000000
--- a/spec/frontend/monitoring/graph_data.js
+++ /dev/null
@@ -1,274 +0,0 @@
-import { panelTypes, metricStates } from '~/monitoring/constants';
-import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
-
-const initTime = 1435781450; // "Wed, 01 Jul 2015 20:10:50 GMT"
-const intervalSeconds = 120;
-
-const makeValue = (val) => [initTime, val];
-const makeValues = (vals) => vals.map((val, i) => [initTime + intervalSeconds * i, val]);
-
-// Raw Promethues Responses
-
-export const prometheusMatrixMultiResult = ({
- values1 = ['1', '2', '3'],
- values2 = ['4', '5', '6'],
-} = {}) => ({
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: makeValues(values1),
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: makeValues(values2),
- },
- ],
-});
-
-// Normalized Prometheus Responses
-
-const scalarResult = ({ value = '1' } = {}) =>
- normalizeQueryResponseData({
- resultType: 'scalar',
- result: makeValue(value),
- });
-
-const vectorResult = ({ value1 = '1', value2 = '2' } = {}) =>
- normalizeQueryResponseData({
- resultType: 'vector',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- value: makeValue(value1),
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9100',
- },
- value: makeValue(value2),
- },
- ],
- });
-
-const matrixSingleResult = ({ values = ['1', '2', '3'] } = {}) =>
- normalizeQueryResponseData({
- resultType: 'matrix',
- result: [
- {
- metric: {},
- values: makeValues(values),
- },
- ],
- });
-
-const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'] } = {}) =>
- normalizeQueryResponseData({
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: makeValues(values1),
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: makeValues(values2),
- },
- ],
- });
-
-// GraphData factory
-
-/**
- * Generate mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- * @param {Object} dataOptions.metricCount
- * @param {Object} dataOptions.isMultiSeries
- */
-export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Time Series Panel',
- type: panelTypes.LINE_CHART,
- x_label: 'X Axis',
- y_label: 'Y Axis',
- metrics: Array.from(Array(metricCount), (_, i) => ({
- label: withLabels ? `Metric ${i + 1}` : undefined,
- state: metricStates.OK,
- result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
- })),
- ...panelOptions,
- });
-};
-
-/**
- * Generate mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- * @param {Object} dataOptions.unit
- * @param {Object} dataOptions.value
- * @param {Object} dataOptions.isVector
- */
-export const singleStatGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { unit, value = '1', isVector = false } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Single Stat Panel',
- type: panelTypes.SINGLE_STAT,
- metrics: [
- {
- label: 'Metric Label',
- state: metricStates.OK,
- result: isVector ? vectorResult({ value }) : scalarResult({ value }),
- unit,
- },
- ],
- ...panelOptions,
- });
-};
-
-/**
- * Generate mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- * @param {Array} dataOptions.values - Metric values
- * @param {Array} dataOptions.upper - Upper boundary values
- * @param {Array} dataOptions.lower - Lower boundary values
- */
-export const anomalyGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { values, upper, lower } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Anomaly Panel',
- type: panelTypes.ANOMALY_CHART,
- x_label: 'X Axis',
- y_label: 'Y Axis',
- metrics: [
- {
- label: `Metric`,
- state: metricStates.OK,
- result: matrixSingleResult({ values }),
- },
- {
- label: `Upper boundary`,
- state: metricStates.OK,
- result: matrixSingleResult({ values: upper }),
- },
- {
- label: `Lower boundary`,
- state: metricStates.OK,
- result: matrixSingleResult({ values: lower }),
- },
- ],
- ...panelOptions,
- });
-};
-
-/**
- * Generate mock graph data for heatmaps according to options
- */
-export const heatmapGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { metricCount = 1 } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Heatmap Panel',
- type: panelTypes.HEATMAP,
- x_label: 'X Axis',
- y_label: 'Y Axis',
- metrics: Array.from(Array(metricCount), (_, i) => ({
- label: `Metric ${i + 1}`,
- state: metricStates.OK,
- result: matrixMultiResult(),
- })),
- ...panelOptions,
- });
-};
-
-/**
- * Generate gauge chart mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- *
- */
-export const gaugeChartGraphData = (panelOptions = {}) => {
- const {
- minValue = 100,
- maxValue = 1000,
- split = 20,
- thresholds = {
- mode: 'absolute',
- values: [500, 800],
- },
- format = 'kilobytes',
- } = panelOptions;
-
- return mapPanelToViewModel({
- title: 'Gauge Chart Panel',
- type: panelTypes.GAUGE_CHART,
- min_value: minValue,
- max_value: maxValue,
- split,
- thresholds,
- format,
- metrics: [
- {
- label: `Metric`,
- state: metricStates.OK,
- result: matrixSingleResult(),
- },
- ],
- });
-};
-
-/**
- * Generates stacked mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- */
-export const stackedColumnGraphData = (panelOptions = {}, dataOptions = {}) => {
- return {
- ...timeSeriesGraphData(panelOptions, dataOptions),
- type: panelTypes.STACKED_COLUMN,
- };
-};
-
-/**
- * Generates bar mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- */
-export const barGraphData = (panelOptions = {}, dataOptions = {}) => {
- return {
- ...timeSeriesGraphData(panelOptions, dataOptions),
- type: panelTypes.BAR,
- };
-};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
deleted file mode 100644
index 1d23190e586..00000000000
--- a/spec/frontend/monitoring/mock_data.js
+++ /dev/null
@@ -1,574 +0,0 @@
-// The path below needs to be relative because we import the mock-data to karma
-import invalidUrl from '~/lib/utils/invalid_url';
-import { TEST_HOST } from '../__helpers__/test_constants';
-// This import path needs to be relative for now because this mock data is used in
-// Karma specs too, where the helpers/test_constants alias can not be resolved
-
-export const mockProjectDir = '/frontend-fixtures/environments-project';
-export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`;
-
-export const customDashboardBasePath = '.gitlab/dashboards';
-
-const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
- default: false,
- display_name: `Custom Dashboard ${idx}`,
- can_edit: true,
- system_dashboard: false,
- out_of_the_box_dashboard: false,
- project_blob_path: `${mockProjectDir}/blob/main/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
- path: `.gitlab/dashboards/dashboard_${idx}.yml`,
- starred: false,
-}));
-
-export const mockDashboardsErrorResponse = {
- all_dashboards: customDashboardsData,
- message: "Each 'panel_group' must define an array :panels",
- status: 'error',
-};
-
-export const anomalyDeploymentData = [
- {
- id: 111,
- iid: 3,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-08-19T22:00:00.000Z',
- deployed_at: '2019-08-19T22:01:00.000Z',
- tag: false,
- 'last?': true,
- },
- {
- id: 110,
- iid: 2,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-08-19T23:00:00.000Z',
- deployed_at: '2019-08-19T23:00:00.000Z',
- tag: false,
- 'last?': false,
- },
-];
-
-export const deploymentData = [
- {
- id: 111,
- iid: 3,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/-/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-07-16T10:14:25.589Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': true,
- },
- {
- id: 110,
- iid: 2,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/-/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-07-16T11:14:25.589Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': false,
- },
- {
- id: 109,
- iid: 1,
- sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/-/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
- ref: {
- name: 'update2-readme',
- },
- created_at: '2019-07-16T12:14:25.589Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': false,
- },
-];
-
-export const annotationsData = [
- {
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
- startingAt: '2020-04-12 12:51:53 UTC',
- endingAt: null,
- panelId: null,
- description: 'This is a test annotation',
- },
- {
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/2',
- description: 'test annotation 2',
- startingAt: '2020-04-13 12:51:53 UTC',
- endingAt: null,
- panelId: null,
- },
- {
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/3',
- description: 'test annotation 3',
- startingAt: '2020-04-16 12:51:53 UTC',
- endingAt: null,
- panelId: null,
- },
-];
-
-const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({
- id: `gid://gitlab/Environments/${150 + idx}`,
- name: `no-deployment/noop-branch-${idx}`,
- state: 'available',
- created_at: '2018-07-04T18:39:41.702Z',
- updated_at: '2018-07-04T18:44:54.010Z',
-}));
-
-export const environmentData = [
- {
- id: 'gid://gitlab/Environments/34',
- name: 'production',
- state: 'available',
- external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
- environment_type: null,
- stop_action: false,
- metrics_path: '/root/hello-prometheus/environments/34/metrics',
- environment_path: '/root/hello-prometheus/environments/34',
- stop_path: '/root/hello-prometheus/environments/34/stop',
- terminal_path: '/root/hello-prometheus/environments/34/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/production',
- created_at: '2018-06-29T16:53:38.301Z',
- updated_at: '2018-06-29T16:57:09.825Z',
- last_deployment: {
- id: 127,
- },
- },
- {
- id: 'gid://gitlab/Environments/35',
- name: 'review/noop-branch',
- state: 'available',
- external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
- environment_type: 'review',
- stop_action: true,
- metrics_path: '/root/hello-prometheus/environments/35/metrics',
- environment_path: '/root/hello-prometheus/environments/35',
- stop_path: '/root/hello-prometheus/environments/35/stop',
- terminal_path: '/root/hello-prometheus/environments/35/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/review',
- created_at: '2018-07-03T18:39:41.702Z',
- updated_at: '2018-07-03T18:44:54.010Z',
- last_deployment: {
- id: 128,
- },
- },
-].concat(extraEnvironmentData);
-
-export const dashboardGitResponse = [
- {
- default: true,
- display_name: 'Overview',
- can_edit: false,
- system_dashboard: true,
- out_of_the_box_dashboard: true,
- project_blob_path: null,
- path: 'config/prometheus/common_metrics.yml',
- starred: false,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
- },
- {
- default: false,
- display_name: 'dashboard.yml',
- can_edit: true,
- system_dashboard: false,
- out_of_the_box_dashboard: false,
- project_blob_path: `${mockProjectDir}/-/blob/main/.gitlab/dashboards/dashboard.yml`,
- path: '.gitlab/dashboards/dashboard.yml',
- starred: true,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
- },
- {
- default: false,
- display_name: 'Pod Health',
- can_edit: false,
- system_dashboard: false,
- out_of_the_box_dashboard: true,
- project_blob_path: null,
- path: 'config/prometheus/pod_metrics.yml',
- starred: false,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/pod_metrics.yml`,
- },
- ...customDashboardsData,
-];
-
-// Metrics mocks
-
-export const metricsResult = [
- {
- metric: {},
- values: [
- [1563272065.589, '10.396484375'],
- [1563272125.589, '10.333984375'],
- [1563272185.589, '10.333984375'],
- [1563272245.589, '10.333984375'],
- ],
- },
-];
-
-export const barMockData = {
- title: 'SLA Trends - Primary Services',
- type: 'bar',
- xLabel: 'service',
- y_label: 'percentile',
- metrics: [
- {
- id: 'sla_trends_primary_services',
- series_name: 'group 1',
- metricId: 'NO_DB_sla_trends_primary_services',
- query_range:
- 'avg(avg_over_time(slo_observation_status{environment="gprd", stage=~"main|", type=~"api|web|git|registry|sidekiq|ci-runners"}[1d])) by (type)',
- unit: 'Percentile',
- label: 'SLA',
- prometheus_endpoint_path:
- '/gitlab-com/metrics-dogfooding/-/environments/266/prometheus/api/v1/query_range?query=clamp_min%28clamp_max%28avg%28avg_over_time%28slo_observation_status%7Benvironment%3D%22gprd%22%2C+stage%3D~%22main%7C%22%2C+type%3D~%22api%7Cweb%7Cgit%7Cregistry%7Csidekiq%7Cci-runners%22%7D%5B1d%5D%29%29+by+%28type%29%2C1%29%2C0%29',
- result: [
- {
- metric: { type: 'api' },
- values: [[1583995208, '0.9935198135198128']],
- },
- {
- metric: { type: 'git' },
- values: [[1583995208, '0.9975296513504401']],
- },
- {
- metric: { type: 'registry' },
- values: [[1583995208, '0.9994716394716395']],
- },
- {
- metric: { type: 'sidekiq' },
- values: [[1583995208, '0.9948251748251747']],
- },
- {
- metric: { type: 'web' },
- values: [[1583995208, '0.9535664335664336']],
- },
- {
- metric: { type: 'postgresql_database' },
- values: [[1583995208, '0.9335664335664336']],
- },
- ],
- },
- ],
-};
-
-export const baseNamespace = 'monitoringDashboard';
-
-export const mockNamespace = `${baseNamespace}/1`;
-
-export const mockNamespaces = [`${baseNamespace}/1`, `${baseNamespace}/2`];
-
-export const mockTimeRange = { duration: { seconds: 120 } };
-
-export const mockFixedTimeRange = {
- start: '2020-06-17T19:59:08.659Z',
- end: '2020-07-17T19:59:08.659Z',
-};
-
-export const mockNamespacedData = {
- mockDeploymentData: ['mockDeploymentData'],
- mockProjectPath: '/mockProjectPath',
-};
-
-export const mockLogsPath = '/mockLogsPath';
-
-export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
-
-export const mockLinks = [
- {
- title: 'Job',
- url: 'http://intel.com/bibendum/felis/sed/interdum/venenatis.png',
- },
- {
- title: 'Solarbreeze',
- url: 'http://ebay.co.uk/primis/in/faucibus.jsp',
- },
- {
- title: 'Bentosanzap',
- url: 'http://cargocollective.com/sociis/natoque/penatibus/et/magnis/dis.js',
- },
- {
- title: 'Wrapsafe',
- url: 'https://bloomberg.com/tempus/vel/pede/morbi.aspx',
- },
- {
- title: 'Stronghold',
- url: 'https://networkadvertising.org/primis/in/faucibus/orci/luctus/et/ultrices.html',
- },
- {
- title: 'Lotstring',
- url:
- 'https://huffingtonpost.com/sapien/a/libero.aspx?et=lacus&ultrices=at&posuere=velit&cubilia=vivamus&curae=vel&duis=nulla&faucibus=eget&accumsan=eros&odio=elementum&curabitur=pellentesque&convallis=quisque&duis=porta&consequat=volutpat&dui=erat&nec=quisque&nisi=erat&volutpat=eros&eleifend=viverra&donec=eget&ut=congue&dolor=eget&morbi=semper&vel=rutrum&lectus=nulla&in=nunc&quam=purus&fringilla=phasellus&rhoncus=in&mauris=felis&enim=donec&leo=semper&rhoncus=sapien&sed=a&vestibulum=libero&sit=nam&amet=dui&cursus=proin&id=leo&turpis=odio&integer=porttitor&aliquet=id&massa=consequat&id=in&lobortis=consequat&convallis=ut&tortor=nulla&risus=sed&dapibus=accumsan&augue=felis&vel=ut&accumsan=at&tellus=dolor&nisi=quis&eu=odio',
- },
- {
- title: 'Cardify',
- url:
- 'http://nature.com/imperdiet/et/commodo/vulputate/justo/in/blandit.json?tempus=posuere&semper=felis&est=sed&quam=lacus&pharetra=morbi&magna=sem&ac=mauris&consequat=laoreet&metus=ut&sapien=rhoncus&ut=aliquet&nunc=pulvinar&vestibulum=sed&ante=nisl&ipsum=nunc&primis=rhoncus&in=dui&faucibus=vel&orci=sem&luctus=sed&et=sagittis&ultrices=nam&posuere=congue&cubilia=risus&curae=semper&mauris=porta&viverra=volutpat&diam=quam&vitae=pede&quam=lobortis&suspendisse=ligula&potenti=sit&nullam=amet&porttitor=eleifend&lacus=pede&at=libero&turpis=quis',
- },
- {
- title: 'Ventosanzap',
- url:
- 'http://stanford.edu/augue/vestibulum/ante/ipsum/primis/in/faucibus.xml?metus=morbi&sapien=quis&ut=tortor&nunc=id&vestibulum=nulla&ante=ultrices&ipsum=aliquet&primis=maecenas&in=leo&faucibus=odio&orci=condimentum&luctus=id&et=luctus&ultrices=nec&posuere=molestie&cubilia=sed&curae=justo&mauris=pellentesque&viverra=viverra&diam=pede&vitae=ac&quam=diam&suspendisse=cras&potenti=pellentesque&nullam=volutpat&porttitor=dui&lacus=maecenas&at=tristique&turpis=est&donec=et&posuere=tempus&metus=semper&vitae=est&ipsum=quam&aliquam=pharetra&non=magna&mauris=ac&morbi=consequat&non=metus',
- },
- {
- title: 'Cardguard',
- url:
- 'https://google.com.hk/lacinia/eget/tincidunt/eget/tempus/vel.js?at=eget&turpis=nunc&a=donec',
- },
- {
- title: 'Namfix',
- url:
- 'https://fotki.com/eget/rutrum/at/lorem.jsp?at=id&vulputate=nulla&vitae=ultrices&nisl=aliquet&aenean=maecenas&lectus=leo&pellentesque=odio&eget=condimentum&nunc=id&donec=luctus&quis=nec&orci=molestie&eget=sed&orci=justo&vehicula=pellentesque&condimentum=viverra&curabitur=pede&in=ac&libero=diam&ut=cras&massa=pellentesque&volutpat=volutpat&convallis=dui&morbi=maecenas&odio=tristique&odio=est&elementum=et&eu=tempus&interdum=semper&eu=est&tincidunt=quam&in=pharetra&leo=magna&maecenas=ac&pulvinar=consequat&lobortis=metus&est=sapien&phasellus=ut&sit=nunc&amet=vestibulum&erat=ante&nulla=ipsum&tempus=primis&vivamus=in&in=faucibus&felis=orci&eu=luctus&sapien=et&cursus=ultrices&vestibulum=posuere&proin=cubilia&eu=curae&mi=mauris&nulla=viverra&ac=diam&enim=vitae&in=quam&tempor=suspendisse&turpis=potenti&nec=nullam&euismod=porttitor&scelerisque=lacus&quam=at&turpis=turpis&adipiscing=donec&lorem=posuere&vitae=metus&mattis=vitae&nibh=ipsum&ligula=aliquam&nec=non&sem=mauris&duis=morbi&aliquam=non&convallis=lectus&nunc=aliquam&proin=sit&at=amet',
- },
- {
- title: 'Alpha',
- url:
- 'http://bravesites.com/tempus/vel.jpg?risus=est&auctor=phasellus&sed=sit&tristique=amet&in=erat&tempus=nulla&sit=tempus&amet=vivamus&sem=in&fusce=felis&consequat=eu&nulla=sapien&nisl=cursus&nunc=vestibulum&nisl=proin&duis=eu&bibendum=mi&felis=nulla&sed=ac&interdum=enim&venenatis=in&turpis=tempor&enim=turpis&blandit=nec&mi=euismod&in=scelerisque&porttitor=quam&pede=turpis&justo=adipiscing&eu=lorem&massa=vitae&donec=mattis&dapibus=nibh&duis=ligula',
- },
- {
- title: 'Sonsing',
- url:
- 'http://microsoft.com/blandit.js?quis=ante&lectus=vestibulum&suspendisse=ante&potenti=ipsum&in=primis&eleifend=in&quam=faucibus&a=orci&odio=luctus&in=et&hac=ultrices&habitasse=posuere&platea=cubilia&dictumst=curae&maecenas=duis&ut=faucibus&massa=accumsan&quis=odio&augue=curabitur&luctus=convallis&tincidunt=duis&nulla=consequat&mollis=dui&molestie=nec&lorem=nisi&quisque=volutpat&ut=eleifend&erat=donec&curabitur=ut&gravida=dolor&nisi=morbi&at=vel&nibh=lectus&in=in&hac=quam&habitasse=fringilla&platea=rhoncus&dictumst=mauris&aliquam=enim&augue=leo&quam=rhoncus&sollicitudin=sed&vitae=vestibulum&consectetuer=sit&eget=amet&rutrum=cursus&at=id&lorem=turpis&integer=integer&tincidunt=aliquet&ante=massa&vel=id&ipsum=lobortis&praesent=convallis&blandit=tortor&lacinia=risus&erat=dapibus&vestibulum=augue&sed=vel&magna=accumsan&at=tellus&nunc=nisi&commodo=eu&placerat=orci&praesent=mauris&blandit=lacinia&nam=sapien&nulla=quis&integer=libero',
- },
- {
- title: 'Fintone',
- url:
- 'https://linkedin.com/duis/bibendum/felis/sed/interdum/venenatis.json?ut=justo&suscipit=sollicitudin&a=ut&feugiat=suscipit&et=a&eros=feugiat&vestibulum=et&ac=eros&est=vestibulum&lacinia=ac&nisi=est&venenatis=lacinia&tristique=nisi&fusce=venenatis&congue=tristique&diam=fusce&id=congue&ornare=diam&imperdiet=id&sapien=ornare&urna=imperdiet&pretium=sapien&nisl=urna&ut=pretium&volutpat=nisl&sapien=ut&arcu=volutpat&sed=sapien&augue=arcu&aliquam=sed&erat=augue&volutpat=aliquam&in=erat&congue=volutpat&etiam=in&justo=congue&etiam=etiam&pretium=justo&iaculis=etiam&justo=pretium&in=iaculis&hac=justo&habitasse=in&platea=hac&dictumst=habitasse&etiam=platea&faucibus=dictumst&cursus=etiam&urna=faucibus&ut=cursus&tellus=urna&nulla=ut&ut=tellus&erat=nulla&id=ut&mauris=erat&vulputate=id&elementum=mauris&nullam=vulputate&varius=elementum&nulla=nullam&facilisi=varius&cras=nulla&non=facilisi&velit=cras&nec=non&nisi=velit&vulputate=nec&nonummy=nisi&maecenas=vulputate&tincidunt=nonummy&lacus=maecenas&at=tincidunt&velit=lacus&vivamus=at&vel=velit&nulla=vivamus&eget=vel&eros=nulla&elementum=eget',
- },
- {
- title: 'Fix San',
- url:
- 'http://pinterest.com/mi/in/porttitor/pede.png?varius=nibh&integer=quisque&ac=id&leo=justo&pellentesque=sit&ultrices=amet&mattis=sapien&odio=dignissim&donec=vestibulum&vitae=vestibulum&nisi=ante&nam=ipsum&ultrices=primis&libero=in&non=faucibus&mattis=orci&pulvinar=luctus&nulla=et&pede=ultrices&ullamcorper=posuere&augue=cubilia&a=curae&suscipit=nulla&nulla=dapibus&elit=dolor&ac=vel&nulla=est&sed=donec&vel=odio&enim=justo&sit=sollicitudin&amet=ut&nunc=suscipit&viverra=a&dapibus=feugiat&nulla=et&suscipit=eros&ligula=vestibulum&in=ac&lacus=est&curabitur=lacinia&at=nisi&ipsum=venenatis&ac=tristique&tellus=fusce&semper=congue&interdum=diam&mauris=id&ullamcorper=ornare&purus=imperdiet&sit=sapien&amet=urna&nulla=pretium&quisque=nisl&arcu=ut&libero=volutpat&rutrum=sapien&ac=arcu&lobortis=sed&vel=augue&dapibus=aliquam&at=erat&diam=volutpat&nam=in&tristique=congue&tortor=etiam',
- },
- {
- title: 'Ronstring',
- url:
- 'https://ebay.com/ut/erat.aspx?nulla=sed&eget=nisl&eros=nunc&elementum=rhoncus&pellentesque=dui&quisque=vel&porta=sem&volutpat=sed&erat=sagittis&quisque=nam&erat=congue&eros=risus&viverra=semper&eget=porta&congue=volutpat&eget=quam&semper=pede&rutrum=lobortis&nulla=ligula',
- },
- {
- title: 'It',
- url:
- 'http://symantec.com/tortor/sollicitudin/mi/sit/amet.json?in=nullam&libero=varius&ut=nulla&massa=facilisi&volutpat=cras&convallis=non&morbi=velit&odio=nec&odio=nisi&elementum=vulputate&eu=nonummy&interdum=maecenas&eu=tincidunt&tincidunt=lacus&in=at&leo=velit&maecenas=vivamus&pulvinar=vel&lobortis=nulla&est=eget&phasellus=eros&sit=elementum&amet=pellentesque&erat=quisque&nulla=porta&tempus=volutpat&vivamus=erat&in=quisque&felis=erat&eu=eros&sapien=viverra&cursus=eget&vestibulum=congue&proin=eget&eu=semper',
- },
- {
- title: 'Andalax',
- url:
- 'https://acquirethisname.com/tortor/eu.js?volutpat=mauris&dui=laoreet&maecenas=ut&tristique=rhoncus&est=aliquet&et=pulvinar&tempus=sed&semper=nisl&est=nunc&quam=rhoncus&pharetra=dui&magna=vel&ac=sem&consequat=sed&metus=sagittis&sapien=nam&ut=congue&nunc=risus&vestibulum=semper&ante=porta&ipsum=volutpat&primis=quam&in=pede&faucibus=lobortis&orci=ligula&luctus=sit&et=amet&ultrices=eleifend&posuere=pede&cubilia=libero&curae=quis&mauris=orci&viverra=nullam&diam=molestie&vitae=nibh&quam=in&suspendisse=lectus&potenti=pellentesque&nullam=at&porttitor=nulla&lacus=suspendisse&at=potenti&turpis=cras&donec=in&posuere=purus&metus=eu&vitae=magna&ipsum=vulputate&aliquam=luctus&non=cum&mauris=sociis&morbi=natoque&non=penatibus&lectus=et&aliquam=magnis&sit=dis&amet=parturient&diam=montes&in=nascetur&magna=ridiculus&bibendum=mus',
- },
-];
-
-export const templatingVariablesExamples = {
- text: {
- textSimple: 'My default value',
- textAdvanced: {
- label: 'Advanced text variable',
- type: 'text',
- options: {
- default_value: 'A default value',
- },
- },
- },
- custom: {
- customSimple: ['value1', 'value2', 'value3'],
- customAdvanced: {
- label: 'Advanced Var',
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- customAdvancedWithoutOpts: {
- type: 'custom',
- options: {},
- },
- customAdvancedWithoutLabel: {
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- customAdvancedWithoutType: {
- label: 'Variable 2',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- customAdvancedWithoutOptText: {
- label: 'Options without text',
- type: 'custom',
- options: {
- values: [
- { value: 'value1' },
- {
- value: 'value2',
- default: true,
- },
- ],
- },
- },
- },
- metricLabelValues: {
- metricLabelValuesSimple: {
- label: 'Metric Label Values',
- type: 'metric_label_values',
- options: {
- prometheus_endpoint_path: '/series',
- series_selector: 'backend:haproxy_backend_availability:ratio{env="{{env}}"}',
- label: 'backend',
- },
- },
- },
-};
-
-export const storeTextVariables = [
- {
- type: 'text',
- name: 'textSimple',
- label: 'textSimple',
- value: 'My default value',
- },
- {
- type: 'text',
- name: 'textAdvanced',
- label: 'Advanced text variable',
- value: 'A default value',
- },
-];
-
-export const storeCustomVariables = [
- {
- type: 'custom',
- name: 'customSimple',
- label: 'customSimple',
- options: {
- values: [
- { default: false, text: 'value1', value: 'value1' },
- { default: false, text: 'value2', value: 'value2' },
- { default: false, text: 'value3', value: 'value3' },
- ],
- },
- value: 'value1',
- },
- {
- type: 'custom',
- name: 'customAdvanced',
- label: 'Advanced Var',
- options: {
- values: [
- { default: false, text: 'Var 1 Option 1', value: 'value1' },
- { default: true, text: 'Var 1 Option 2', value: 'value2' },
- ],
- },
- value: 'value2',
- },
- {
- type: 'custom',
- name: 'customAdvancedWithoutOpts',
- label: 'customAdvancedWithoutOpts',
- options: { values: [] },
- value: null,
- },
- {
- type: 'custom',
- name: 'customAdvancedWithoutLabel',
- label: 'customAdvancedWithoutLabel',
- value: 'value2',
- options: {
- values: [
- { default: false, text: 'Var 1 Option 1', value: 'value1' },
- { default: true, text: 'Var 1 Option 2', value: 'value2' },
- ],
- },
- },
- {
- type: 'custom',
- name: 'customAdvancedWithoutOptText',
- label: 'Options without text',
- options: {
- values: [
- { default: false, text: 'value1', value: 'value1' },
- { default: true, text: 'value2', value: 'value2' },
- ],
- },
- value: 'value2',
- },
-];
-
-export const storeMetricLabelValuesVariables = [
- {
- type: 'metric_label_values',
- name: 'metricLabelValuesSimple',
- label: 'Metric Label Values',
- options: { prometheusEndpointPath: '/series', label: 'backend', values: [] },
- value: null,
- },
-];
-
-export const storeVariables = [
- ...storeTextVariables,
- ...storeCustomVariables,
- ...storeMetricLabelValuesVariables,
-];
-
-export const dashboardHeaderProps = {
- defaultBranch: 'main',
- isRearrangingPanels: false,
- selectedTimeRange: {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- },
-};
-
-export const dashboardActionsMenuProps = {
- defaultBranch: 'main',
- addingMetricsAvailable: true,
- customMetricsPath: 'https://path/to/customMetrics',
- validateQueryPath: 'https://path/to/validateQuery',
- isOotbDashboard: true,
-};
-
-export const mockAlert = {
- alert_path: 'alert_path',
- id: 8,
- metricId: 'mock_metric_id',
- operator: '>',
- query: 'testQuery',
- runbookUrl: invalidUrl,
- threshold: 5,
- title: 'alert title',
-};
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
deleted file mode 100644
index 7fcb7607772..00000000000
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
-import { createStore } from '~/monitoring/stores';
-import { assertProps } from 'helpers/assert_props';
-import { dashboardProps } from '../fixture_data';
-
-describe('monitoring/pages/dashboard_page', () => {
- let wrapper;
- let store;
- let $route;
-
- const buildRouter = () => {
- const dashboard = {};
- $route = {
- params: { dashboard },
- query: { dashboard },
- };
- };
-
- const buildWrapper = (props = {}) => {
- wrapper = shallowMount(DashboardPage, {
- store,
- propsData: {
- ...props,
- },
- mocks: {
- $route,
- },
- });
- };
-
- const findDashboardComponent = () => wrapper.findComponent(Dashboard);
-
- beforeEach(() => {
- buildRouter();
- store = createStore();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- it('throws errors if dashboard props are not passed', () => {
- expect(() => assertProps(DashboardPage, {})).toThrow('Missing required prop: "dashboardProps"');
- });
-
- it('renders the dashboard page with dashboard component', () => {
- buildWrapper({ dashboardProps });
-
- const allProps = {
- ...dashboardProps,
- // default props values
- rearrangePanelsAvailable: false,
- showHeader: true,
- showPanels: true,
- smallEmptyState: false,
- };
-
- expect(findDashboardComponent().exists()).toBe(true);
- expect(allProps).toMatchObject(findDashboardComponent().props());
- });
-});
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
deleted file mode 100644
index 98ee6c1cb29..00000000000
--- a/spec/frontend/monitoring/pages/panel_new_page_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
-import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
-import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
-import { createStore } from '~/monitoring/stores';
-
-const dashboard = 'dashboard.yml';
-
-// Button stub that can accept `to` as router links do
-// https://bootstrap-vue.org/docs/components/button#comp-ref-b-button-props
-const GlButtonStub = {
- extends: GlButton,
- props: {
- to: [String, Object],
- },
-};
-
-describe('monitoring/pages/panel_new_page', () => {
- let store;
- let wrapper;
- let $route;
- let $router;
-
- const mountComponent = (propsData = {}, route) => {
- $route = route ?? { name: PANEL_NEW_PAGE, params: { dashboard } };
- $router = {
- push: jest.fn(),
- };
-
- wrapper = shallowMount(PanelNewPage, {
- propsData,
- store,
- stubs: {
- GlButton: GlButtonStub,
- },
- mocks: {
- $router,
- $route,
- },
- });
- };
-
- const findBackButton = () => wrapper.findComponent(GlButtonStub);
- const findPanelBuilder = () => wrapper.findComponent(DashboardPanelBuilder);
-
- beforeEach(() => {
- store = createStore();
- mountComponent();
- });
-
- describe('back to dashboard button', () => {
- it('is rendered', () => {
- expect(findBackButton().exists()).toBe(true);
- expect(findBackButton().props('icon')).toBe('go-back');
- });
-
- it('links back to the dashboard', () => {
- expect(findBackButton().props('to')).toEqual({
- name: DASHBOARD_PAGE,
- params: { dashboard },
- });
- });
-
- it('links back to the dashboard while preserving query params', () => {
- $route = {
- name: PANEL_NEW_PAGE,
- params: { dashboard },
- query: { another: 'param' },
- };
-
- mountComponent({}, $route);
-
- expect(findBackButton().props('to')).toEqual({
- name: DASHBOARD_PAGE,
- params: { dashboard },
- query: { another: 'param' },
- });
- });
- });
-
- describe('dashboard panel builder', () => {
- it('is rendered', () => {
- expect(findPanelBuilder().exists()).toBe(true);
- });
- });
-
- describe('page routing', () => {
- it('route is not updated by default', () => {
- expect($router.push).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
deleted file mode 100644
index 308895768a4..00000000000
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { backoffMockImplementation } from 'helpers/backoff_helper';
-import axios from '~/lib/utils/axios_utils';
-import * as commonUtils from '~/lib/utils/common_utils';
-import {
- HTTP_STATUS_BAD_REQUEST,
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- HTTP_STATUS_NO_CONTENT,
- HTTP_STATUS_OK,
- HTTP_STATUS_SERVICE_UNAVAILABLE,
- HTTP_STATUS_UNAUTHORIZED,
- HTTP_STATUS_UNPROCESSABLE_ENTITY,
-} from '~/lib/utils/http_status';
-import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
-import { metricsDashboardResponse } from '../fixture_data';
-
-describe('monitoring metrics_requests', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
- });
-
- afterEach(() => {
- mock.reset();
-
- commonUtils.backOff.mockReset();
- });
-
- describe('getDashboard', () => {
- const response = metricsDashboardResponse;
- const dashboardEndpoint = '/dashboard';
- const params = {
- start_time: 'start_time',
- end_time: 'end_time',
- };
-
- it('returns a dashboard response', () => {
- mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_OK, response);
-
- return getDashboard(dashboardEndpoint, params).then((data) => {
- expect(data).toEqual(metricsDashboardResponse);
- });
- });
-
- it('returns a dashboard response after retrying twice', () => {
- mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_OK, response);
-
- return getDashboard(dashboardEndpoint, params).then((data) => {
- expect(data).toEqual(metricsDashboardResponse);
- expect(mock.history.get).toHaveLength(3);
- });
- });
-
- it('rejects after getting an error', () => {
- mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- return getDashboard(dashboardEndpoint, params).catch((error) => {
- expect(error).toEqual(expect.any(Error));
- expect(mock.history.get).toHaveLength(1);
- });
- });
- });
-
- describe('getPrometheusQueryData', () => {
- const response = {
- status: 'success',
- data: {
- resultType: 'matrix',
- result: [],
- },
- };
- const prometheusEndpoint = '/query_range';
- const params = {
- start_time: 'start_time',
- end_time: 'end_time',
- };
-
- it('returns a dashboard response', () => {
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_OK, response);
-
- return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
- expect(data).toEqual(response.data);
- });
- });
-
- it('returns a dashboard response after retrying twice', () => {
- // Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_OK, response); // 3rd attempt
-
- return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
- expect(data).toEqual(response.data);
- expect(mock.history.get).toHaveLength(3);
- });
- });
-
- it('rejects after getting an HTTP 500 error', () => {
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
- status: 'error',
- error: 'An error occurred',
- });
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error('Request failed with status code 500'));
- });
- });
-
- it('rejects after retrying twice and getting an HTTP 401 error', () => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_UNAUTHORIZED, {
- status: 'error',
- error: 'An error occurred',
- });
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error('Request failed with status code 401'));
- });
- });
-
- it('rejects after retrying twice and getting an HTTP 500 error', () => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
- status: 'error',
- error: 'An error occurred',
- }); // 3rd attempt
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error('Request failed with status code 500'));
- expect(mock.history.get).toHaveLength(3);
- });
- });
-
- it.each`
- code | reason
- ${HTTP_STATUS_BAD_REQUEST} | ${'Parameters are missing or incorrect'}
- ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
- ${HTTP_STATUS_SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
- `('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
- mock.onGet(prometheusEndpoint).reply(code, {
- status: 'error',
- error: reason,
- });
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error(reason));
- expect(mock.history.get).toHaveLength(1);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
deleted file mode 100644
index 368bd955fb3..00000000000
--- a/spec/frontend/monitoring/router_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
-import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
-import createRouter from '~/monitoring/router';
-import { createStore } from '~/monitoring/stores';
-import { dashboardProps } from './fixture_data';
-import { dashboardHeaderProps } from './mock_data';
-
-const LEGACY_BASE_PATH = '/project/my-group/test-project/-/environments/71146/metrics';
-const BASE_PATH = '/project/my-group/test-project/-/metrics';
-
-const MockApp = {
- data() {
- return {
- dashboardProps: { ...dashboardProps, ...dashboardHeaderProps },
- };
- },
- template: `<router-view :dashboard-props="dashboardProps"/>`,
-};
-
-describe('Monitoring router', () => {
- let router;
- let store;
-
- const createWrapper = (basePath, routeArg) => {
- Vue.use(VueRouter);
-
- router = createRouter(basePath);
- if (routeArg !== undefined) {
- router.push(routeArg);
- }
-
- return mount(MockApp, {
- store,
- router,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- afterEach(() => {
- window.location.hash = '';
- });
-
- describe('support legacy URLs with full dashboard path to visit dashboard page', () => {
- it.each`
- path | currentDashboard
- ${'/dashboard.yml'} | ${'dashboard.yml'}
- ${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
- ${'/?dashboard=dashboard.yml'} | ${'dashboard.yml'}
- `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
- const wrapper = createWrapper(LEGACY_BASE_PATH, path);
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
- currentDashboard,
- });
-
- expect(wrapper.findComponent(DashboardPage).exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true);
- });
- });
-
- describe('supports URLs to visit dashboard page', () => {
- it.each`
- path | currentDashboard
- ${'/'} | ${null}
- ${'/dashboard.yml'} | ${'dashboard.yml'}
- ${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
- ${'/folder1%2Fdashboard.yml'} | ${'folder1/dashboard.yml'}
- ${'/dashboard.yml'} | ${'dashboard.yml'}
- ${'/config/prometheus/common_metrics.yml'} | ${'config/prometheus/common_metrics.yml'}
- ${'/config/prometheus/pod_metrics.yml'} | ${'config/prometheus/pod_metrics.yml'}
- ${'/config%2Fprometheus%2Fpod_metrics.yml'} | ${'config/prometheus/pod_metrics.yml'}
- `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
- const wrapper = createWrapper(BASE_PATH, path);
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
- currentDashboard,
- });
-
- expect(wrapper.findComponent(DashboardPage).exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true);
- });
- });
-
- describe('supports URLs to visit new panel page', () => {
- it.each`
- path | currentDashboard
- ${'/panel/new'} | ${undefined}
- ${'/dashboard.yml/panel/new'} | ${'dashboard.yml'}
- ${'/config/prometheus/common_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
- ${'/config%2Fprometheus%2Fcommon_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
- `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
- const wrapper = createWrapper(BASE_PATH, path);
-
- expect(wrapper.vm.$route.params.dashboard).toBe(currentDashboard);
- expect(wrapper.findComponent(PanelNewPage).exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
deleted file mode 100644
index b3b198d6b51..00000000000
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ /dev/null
@@ -1,1167 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { backoffMockImplementation } from 'helpers/backoff_helper';
-import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import * as commonUtils from '~/lib/utils/common_utils';
-import {
- HTTP_STATUS_BAD_REQUEST,
- HTTP_STATUS_CREATED,
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- HTTP_STATUS_OK,
- HTTP_STATUS_UNPROCESSABLE_ENTITY,
-} from '~/lib/utils/http_status';
-import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
-
-import getAnnotations from '~/monitoring/queries/get_annotations.query.graphql';
-import getDashboardValidationWarnings from '~/monitoring/queries/get_dashboard_validation_warnings.query.graphql';
-import getEnvironments from '~/monitoring/queries/get_environments.query.graphql';
-import { createStore } from '~/monitoring/stores';
-import {
- setGettingStartedEmptyState,
- setInitialState,
- setExpandedPanel,
- clearExpandedPanel,
- filterEnvironments,
- fetchData,
- fetchDashboard,
- receiveMetricsDashboardSuccess,
- fetchDashboardData,
- fetchPrometheusMetric,
- fetchDeploymentsData,
- fetchEnvironmentsData,
- fetchAnnotations,
- fetchDashboardValidationWarnings,
- toggleStarredValue,
- duplicateSystemDashboard,
- updateVariablesAndFetchData,
- fetchVariableMetricLabelValues,
- fetchPanelPreview,
-} from '~/monitoring/stores/actions';
-import * as getters from '~/monitoring/stores/getters';
-import * as types from '~/monitoring/stores/mutation_types';
-import storeState from '~/monitoring/stores/state';
-import {
- gqClient,
- parseEnvironmentsResponse,
- parseAnnotationsResponse,
-} from '~/monitoring/stores/utils';
-import Tracking from '~/tracking';
-import { defaultTimeRange } from '~/vue_shared/constants';
-import {
- metricsDashboardResponse,
- metricsDashboardViewModel,
- metricsDashboardPanelCount,
-} from '../fixture_data';
-import {
- deploymentData,
- environmentData,
- annotationsData,
- dashboardGitResponse,
- mockDashboardsErrorResponse,
-} from '../mock_data';
-
-jest.mock('~/alert');
-
-describe('Monitoring store actions', () => {
- const { convertObjectPropsToCamelCase } = commonUtils;
-
- let mock;
- let store;
- let state;
-
- let dispatch;
- let commit;
-
- beforeEach(() => {
- store = createStore({ getters });
- state = store.state.monitoringDashboard;
- mock = new MockAdapter(axios);
-
- commit = jest.fn();
- dispatch = jest.fn();
-
- jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
- });
-
- afterEach(() => {
- mock.reset();
-
- commonUtils.backOff.mockReset();
- createAlert.mockReset();
- });
-
- // Setup
-
- describe('setGettingStartedEmptyState', () => {
- it('should commit SET_GETTING_STARTED_EMPTY_STATE mutation', () => {
- return testAction(
- setGettingStartedEmptyState,
- null,
- state,
- [
- {
- type: types.SET_GETTING_STARTED_EMPTY_STATE,
- },
- ],
- [],
- );
- });
- });
-
- describe('setInitialState', () => {
- it('should commit SET_INITIAL_STATE mutation', () => {
- return testAction(
- setInitialState,
- {
- currentDashboard: '.gitlab/dashboards/dashboard.yml',
- deploymentsEndpoint: 'deployments.json',
- },
- state,
- [
- {
- type: types.SET_INITIAL_STATE,
- payload: {
- currentDashboard: '.gitlab/dashboards/dashboard.yml',
- deploymentsEndpoint: 'deployments.json',
- },
- },
- ],
- [],
- );
- });
- });
-
- describe('setExpandedPanel', () => {
- it('Sets a panel as expanded', () => {
- const group = 'group_1';
- const panel = { title: 'A Panel' };
-
- return testAction(
- setExpandedPanel,
- { group, panel },
- state,
- [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
- [],
- );
- });
- });
-
- describe('clearExpandedPanel', () => {
- it('Clears a panel as expanded', () => {
- return testAction(
- clearExpandedPanel,
- undefined,
- state,
- [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
- [],
- );
- });
- });
-
- // All Data
-
- describe('fetchData', () => {
- it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
- return testAction(
- fetchData,
- null,
- state,
- [],
- [
- { type: 'fetchEnvironmentsData' },
- { type: 'fetchDashboard' },
- { type: 'fetchAnnotations' },
- ],
- );
- });
-
- it('dispatches when feature metricsDashboardAnnotations is on', () => {
- window.gon = { features: { metricsDashboardAnnotations: true } };
-
- return testAction(
- fetchData,
- null,
- state,
- [],
- [
- { type: 'fetchEnvironmentsData' },
- { type: 'fetchDashboard' },
- { type: 'fetchAnnotations' },
- ],
- );
- });
- });
-
- // Metrics dashboard
-
- describe('fetchDashboard', () => {
- const response = metricsDashboardResponse;
- beforeEach(() => {
- state.dashboardEndpoint = '/dashboard';
- });
-
- it('on success, dispatches receive and success actions, then fetches dashboard warnings', () => {
- document.body.dataset.page = 'projects:environments:metrics';
- mock.onGet(state.dashboardEndpoint).reply(HTTP_STATUS_OK, response);
-
- return testAction(
- fetchDashboard,
- null,
- state,
- [],
- [
- { type: 'requestMetricsDashboard' },
- {
- type: 'receiveMetricsDashboardSuccess',
- payload: { response },
- },
- { type: 'fetchDashboardValidationWarnings' },
- ],
- );
- });
-
- describe('on failure', () => {
- let result;
- beforeEach(() => {
- const params = {};
- const localGetters = {
- fullDashboardPath: store.getters['monitoringDashboard/fullDashboardPath'],
- };
- result = () => {
- mock
- .onGet(state.dashboardEndpoint)
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, mockDashboardsErrorResponse);
- return fetchDashboard({ state, commit, dispatch, getters: localGetters }, params);
- };
- });
-
- it('dispatches a failure', async () => {
- await result();
- expect(commit).toHaveBeenCalledWith(
- types.SET_ALL_DASHBOARDS,
- mockDashboardsErrorResponse.all_dashboards,
- );
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createAlert).toHaveBeenCalled();
- });
-
- it('dispatches a failure action when a message is returned', async () => {
- await result();
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringContaining(mockDashboardsErrorResponse.message),
- });
- });
-
- it('does not show an alert when showErrorBanner is disabled', async () => {
- state.showErrorBanner = false;
-
- await result();
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('receiveMetricsDashboardSuccess', () => {
- it('stores groups', () => {
- const response = metricsDashboardResponse;
- receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response });
- expect(commit).toHaveBeenCalledWith(
- types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
-
- metricsDashboardResponse.dashboard,
- );
- expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
- });
-
- it('sets the dashboards loaded from the repository', () => {
- const params = {};
- const response = metricsDashboardResponse;
- response.all_dashboards = dashboardGitResponse;
- receiveMetricsDashboardSuccess(
- {
- state,
- commit,
- dispatch,
- },
- {
- response,
- params,
- },
- );
- expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
- });
- });
-
- // Metrics
-
- describe('fetchDashboardData', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
-
- state.timeRange = defaultTimeRange;
- });
-
- it('commits empty state when state.groups is empty', async () => {
- const localGetters = {
- metricsWithData: () => [],
- };
- await fetchDashboardData({ state, commit, dispatch, getters: localGetters });
- expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'dashboard_fetch', {
- label: 'custom_metrics_dashboard',
- property: 'count',
- value: 0,
- });
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
- expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
- });
-
- expect(createAlert).not.toHaveBeenCalled();
- });
-
- it('dispatches fetchPrometheusMetric for each panel query', async () => {
- state.dashboard.panelGroups = convertObjectPropsToCamelCase(
- metricsDashboardResponse.dashboard.panel_groups,
- );
-
- const [metric] = state.dashboard.panelGroups[0].panels[0].metrics;
- const localGetters = {
- metricsWithData: () => [metric.id],
- };
-
- await fetchDashboardData({ state, commit, dispatch, getters: localGetters });
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
- metric,
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
- });
-
- expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'dashboard_fetch', {
- label: 'custom_metrics_dashboard',
- property: 'count',
- value: 1,
- });
- });
-
- it('dispatches fetchPrometheusMetric for each panel query, handles an error', async () => {
- state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups;
- const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
-
- dispatch.mockResolvedValueOnce(); // fetchDeploymentsData
- dispatch.mockResolvedValueOnce(); // fetchVariableMetricLabelValues
- // Mock having one out of four metrics failing
- dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
- dispatch.mockResolvedValue();
-
- await fetchDashboardData({ state, commit, dispatch });
- const defaultQueryParams = {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- };
-
- expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 2); // plus 1 for deployments
- expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
- expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
- defaultQueryParams,
- });
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
- metric,
- defaultQueryParams,
- });
-
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('fetchPrometheusMetric', () => {
- const defaultQueryParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- step: 60,
- };
- let metric;
- let data;
- let prometheusEndpointPath;
-
- beforeEach(() => {
- state = storeState();
- [metric] = metricsDashboardViewModel.panelGroups[0].panels[0].metrics;
-
- prometheusEndpointPath = metric.prometheusEndpointPath;
-
- data = {
- metricId: metric.metricId,
- result: [1582065167.353, 5, 1582065599.353],
- };
- });
-
- it('commits result', () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
-
- return testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- );
- });
-
- describe('without metric defined step', () => {
- const expectedParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- step: 60,
- };
-
- it('uses calculated step', async () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
-
- await testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- );
- expect(mock.history.get[0].params).toEqual(expectedParams);
- });
- });
-
- describe('with metric defined step', () => {
- beforeEach(() => {
- metric.step = 7;
- });
-
- const expectedParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- step: 7,
- };
-
- it('uses metric step', async () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
-
- await testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- );
- expect(mock.history.get[0].params).toEqual(expectedParams);
- });
- });
-
- it('commits failure, when waiting for results and getting a server error', async () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- const error = new Error('Request failed with status code 500');
-
- await expect(
- testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_FAILURE,
- payload: {
- metricId: metric.metricId,
- error,
- },
- },
- ],
- [],
- ),
- ).rejects.toEqual(error);
- });
- });
-
- // Deployments
-
- describe('fetchDeploymentsData', () => {
- it('dispatches receiveDeploymentsDataSuccess on success', () => {
- state.deploymentsEndpoint = '/success';
- mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_OK, {
- deployments: deploymentData,
- });
-
- return testAction(
- fetchDeploymentsData,
- null,
- state,
- [],
- [{ type: 'receiveDeploymentsDataSuccess', payload: deploymentData }],
- );
- });
- it('dispatches receiveDeploymentsDataFailure on error', () => {
- state.deploymentsEndpoint = '/error';
- mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- return testAction(
- fetchDeploymentsData,
- null,
- state,
- [],
- [{ type: 'receiveDeploymentsDataFailure' }],
- () => {
- expect(createAlert).toHaveBeenCalled();
- },
- );
- });
- });
-
- // Environments
-
- describe('fetchEnvironmentsData', () => {
- beforeEach(() => {
- state.projectPath = 'gitlab-org/gitlab-test';
- });
-
- it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue({
- data: {
- project: {
- data: {
- environments: [],
- },
- },
- },
- });
-
- return testAction(
- filterEnvironments,
- {},
- state,
- [
- {
- type: 'SET_ENVIRONMENTS_FILTER',
- payload: {},
- },
- ],
- [
- {
- type: 'fetchEnvironmentsData',
- },
- ],
- );
- });
-
- it('fetch environments data call takes in search param', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const searchTerm = 'Something';
- const mutationVariables = {
- mutation: getEnvironments,
- variables: {
- projectPath: state.projectPath,
- search: searchTerm,
- states: [ENVIRONMENT_AVAILABLE_STATE],
- },
- };
- state.environmentsSearchTerm = searchTerm;
- mockMutate.mockResolvedValue({});
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
- [],
- [
- { type: 'requestEnvironmentsData' },
- { type: 'receiveEnvironmentsDataSuccess', payload: [] },
- ],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveEnvironmentsDataSuccess on success', () => {
- jest.spyOn(gqClient, 'mutate').mockResolvedValue({
- data: {
- project: {
- data: {
- environments: environmentData,
- },
- },
- },
- });
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
- [],
- [
- { type: 'requestEnvironmentsData' },
- {
- type: 'receiveEnvironmentsDataSuccess',
- payload: parseEnvironmentsResponse(environmentData, state.projectPath),
- },
- ],
- );
- });
-
- it('dispatches receiveEnvironmentsDataFailure on error', () => {
- jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
- [],
- [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
- );
- });
- });
-
- describe('fetchAnnotations', () => {
- beforeEach(() => {
- state.timeRange = {
- start: '2020-04-15T12:54:32.137Z',
- end: '2020-08-15T12:54:32.137Z',
- };
- state.projectPath = 'gitlab-org/gitlab-test';
- state.currentEnvironmentName = 'production';
- state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
- // testAction doesn't have access to getters. The state is passed in as getters
- // instead of the actual getters inside the testAction method implementation.
- // All methods downstream that needs access to getters will throw and error.
- // For that reason, the result of the getter is set as a state variable.
- state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
- });
-
- it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const mutationVariables = {
- mutation: getAnnotations,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.currentDashboard,
- startingFrom: state.timeRange.start,
- },
- };
- const parsedResponse = parseAnnotationsResponse(annotationsData);
-
- mockMutate.mockResolvedValue({
- data: {
- project: {
- environments: {
- nodes: [
- {
- metricsDashboard: {
- annotations: {
- nodes: parsedResponse,
- },
- },
- },
- ],
- },
- },
- },
- });
-
- return testAction(
- fetchAnnotations,
- null,
- state,
- [],
- [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const mutationVariables = {
- mutation: getAnnotations,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.currentDashboard,
- startingFrom: state.timeRange.start,
- },
- };
-
- mockMutate.mockRejectedValue({});
-
- return testAction(
- fetchAnnotations,
- null,
- state,
- [],
- [{ type: 'receiveAnnotationsFailure' }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
- });
-
- describe('fetchDashboardValidationWarnings', () => {
- let mockMutate;
- let mutationVariables;
-
- beforeEach(() => {
- state.projectPath = 'gitlab-org/gitlab-test';
- state.currentEnvironmentName = 'production';
- state.currentDashboard = '.gitlab/dashboards/dashboard_with_warnings.yml';
- // testAction doesn't have access to getters. The state is passed in as getters
- // instead of the actual getters inside the testAction method implementation.
- // All methods downstream that needs access to getters will throw and error.
- // For that reason, the result of the getter is set as a state variable.
- state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
-
- mockMutate = jest.spyOn(gqClient, 'mutate');
- mutationVariables = {
- mutation: getDashboardValidationWarnings,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.fullDashboardPath,
- },
- };
- });
-
- it('dispatches receiveDashboardValidationWarningsSuccess with true payload when there are warnings', () => {
- mockMutate.mockResolvedValue({
- data: {
- project: {
- id: 'gid://gitlab/Project/29',
- environments: {
- nodes: [
- {
- name: 'production',
- metricsDashboard: {
- path: '.gitlab/dashboards/dashboard_errors_test.yml',
- schemaValidationWarnings: ["unit: can't be blank"],
- },
- },
- ],
- },
- },
- },
- });
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsSuccess', payload: true }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveDashboardValidationWarningsSuccess with false payload when there are no warnings', () => {
- mockMutate.mockResolvedValue({
- data: {
- project: {
- id: 'gid://gitlab/Project/29',
- environments: {
- nodes: [
- {
- name: 'production',
- metricsDashboard: {
- path: '.gitlab/dashboards/dashboard_errors_test.yml',
- schemaValidationWarnings: [],
- },
- },
- ],
- },
- },
- },
- });
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsSuccess', payload: false }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty', () => {
- mockMutate.mockResolvedValue({
- data: {
- project: null,
- },
- });
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsSuccess', payload: false }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveDashboardValidationWarningsFailure if the warnings API call fails', () => {
- mockMutate.mockRejectedValue({});
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsFailure' }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
- });
-
- // Dashboard manipulation
-
- describe('toggleStarredValue', () => {
- let unstarredDashboard;
- let starredDashboard;
-
- beforeEach(() => {
- state.isUpdatingStarredValue = false;
- [unstarredDashboard, starredDashboard] = dashboardGitResponse;
- });
-
- it('performs no changes if no dashboard is selected', () => {
- return testAction(toggleStarredValue, null, state, [], []);
- });
-
- it('performs no changes if already changing starred value', () => {
- state.selectedDashboard = unstarredDashboard;
- state.isUpdatingStarredValue = true;
- return testAction(toggleStarredValue, null, state, [], []);
- });
-
- it('stars dashboard if it is not starred', () => {
- state.selectedDashboard = unstarredDashboard;
- mock.onPost(unstarredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
-
- return testAction(toggleStarredValue, null, state, [
- { type: types.REQUEST_DASHBOARD_STARRING },
- {
- type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS,
- payload: {
- newStarredValue: true,
- selectedDashboard: unstarredDashboard,
- },
- },
- ]);
- });
-
- it('unstars dashboard if it is starred', () => {
- state.selectedDashboard = starredDashboard;
- mock.onPost(starredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
-
- return testAction(toggleStarredValue, null, state, [
- { type: types.REQUEST_DASHBOARD_STARRING },
- { type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
- ]);
- });
- });
-
- describe('duplicateSystemDashboard', () => {
- beforeEach(() => {
- state.dashboardsEndpoint = '/dashboards.json';
- });
-
- it('Succesful POST request resolves', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
- dashboard: dashboardGitResponse[1],
- });
-
- await testAction(duplicateSystemDashboard, {}, state, [], []);
- expect(mock.history.post).toHaveLength(1);
- });
-
- it('Succesful POST request resolves to a dashboard', async () => {
- const mockCreatedDashboard = dashboardGitResponse[1];
-
- const params = {
- dashboard: 'my-dashboard',
- fileName: 'file-name.yml',
- branch: 'my-new-branch',
- commitMessage: 'A new commit message',
- };
-
- const expectedPayload = JSON.stringify({
- dashboard: 'my-dashboard',
- file_name: 'file-name.yml',
- branch: 'my-new-branch',
- commit_message: 'A new commit message',
- });
-
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
- dashboard: mockCreatedDashboard,
- });
-
- const result = await testAction(duplicateSystemDashboard, params, state, [], []);
- expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].data).toEqual(expectedPayload);
- expect(result).toEqual(mockCreatedDashboard);
- });
-
- it('Failed POST request throws an error', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_BAD_REQUEST);
-
- await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
- 'There was an error creating the dashboard.',
- );
- expect(mock.history.post).toHaveLength(1);
- });
-
- it('Failed POST request throws an error with a description', async () => {
- const backendErrorMsg = 'This file already exists!';
-
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_BAD_REQUEST, {
- error: backendErrorMsg,
- });
-
- await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
- `There was an error creating the dashboard. ${backendErrorMsg}`,
- );
- expect(mock.history.post).toHaveLength(1);
- });
- });
-
- // Variables manipulation
-
- describe('updateVariablesAndFetchData', () => {
- it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', () => {
- return testAction(
- updateVariablesAndFetchData,
- { pod: 'POD' },
- state,
- [
- {
- type: types.UPDATE_VARIABLE_VALUE,
- payload: { pod: 'POD' },
- },
- ],
- [
- {
- type: 'fetchDashboardData',
- },
- ],
- );
- });
- });
-
- describe('fetchVariableMetricLabelValues', () => {
- const variable = {
- type: 'metric_label_values',
- name: 'label1',
- options: {
- prometheusEndpointPath: '/series?match[]=metric_name',
- label: 'job',
- },
- };
-
- const defaultQueryParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- };
-
- beforeEach(() => {
- state = {
- ...state,
- timeRange: defaultTimeRange,
- variables: [variable],
- };
- });
-
- it('should commit UPDATE_VARIABLE_METRIC_LABEL_VALUES mutation and fetch data', () => {
- const data = [
- {
- __name__: 'up',
- job: 'prometheus',
- },
- {
- __name__: 'up',
- job: 'POD',
- },
- ];
-
- mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_OK, {
- status: 'success',
- data,
- });
-
- return testAction(
- fetchVariableMetricLabelValues,
- { defaultQueryParams },
- state,
- [
- {
- type: types.UPDATE_VARIABLE_METRIC_LABEL_VALUES,
- payload: { variable, label: 'job', data },
- },
- ],
- [],
- );
- });
-
- it('should notify the user that dynamic options were not loaded', () => {
- mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
- () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringContaining('error getting options for variable "label1"'),
- });
- },
- );
- });
- });
-
- describe('fetchPanelPreview', () => {
- const panelPreviewEndpoint = '/builder.json';
- const mockYmlContent = 'mock yml content';
-
- beforeEach(() => {
- state.panelPreviewEndpoint = panelPreviewEndpoint;
- });
-
- it('should not commit or dispatch if payload is empty', () => {
- testAction(fetchPanelPreview, '', state, [], []);
- });
-
- it('should store the panel and fetch metric results', () => {
- const mockPanel = {
- title: 'Go heap size',
- type: 'area-chart',
- };
-
- mock
- .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(HTTP_STATUS_OK, mockPanel);
-
- testAction(
- fetchPanelPreview,
- mockYmlContent,
- state,
- [
- { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
- { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
- { type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel },
- ],
- [{ type: 'fetchPanelPreviewMetrics' }],
- );
- });
-
- it('should display a validation error when the backend cannot process the yml', () => {
- const mockErrorMsg = 'Each "metric" must define one of :query or :query_range';
-
- mock
- .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {
- message: mockErrorMsg,
- });
-
- testAction(fetchPanelPreview, mockYmlContent, state, [
- { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
- { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
- { type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg },
- ]);
- });
-
- it('should display a generic error when the backend fails', () => {
- mock
- .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- testAction(fetchPanelPreview, mockYmlContent, state, [
- { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
- { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
- {
- type: types.RECEIVE_PANEL_PREVIEW_FAILURE,
- payload: 'Request failed with status code 500',
- },
- ]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/embed_group/actions_spec.js b/spec/frontend/monitoring/store/embed_group/actions_spec.js
deleted file mode 100644
index 5bdfc506cff..00000000000
--- a/spec/frontend/monitoring/store/embed_group/actions_spec.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// import store from '~/monitoring/stores/embed_group';
-import * as actions from '~/monitoring/stores/embed_group/actions';
-import * as types from '~/monitoring/stores/embed_group/mutation_types';
-import { mockNamespace } from '../../mock_data';
-
-describe('Embed group actions', () => {
- describe('addModule', () => {
- it('adds a module to the store', () => {
- const commit = jest.fn();
-
- actions.addModule({ commit }, mockNamespace);
-
- expect(commit).toHaveBeenCalledWith(types.ADD_MODULE, mockNamespace);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/embed_group/getters_spec.js b/spec/frontend/monitoring/store/embed_group/getters_spec.js
deleted file mode 100644
index e3241e41f5e..00000000000
--- a/spec/frontend/monitoring/store/embed_group/getters_spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { metricsWithData } from '~/monitoring/stores/embed_group/getters';
-import { mockNamespaces } from '../../mock_data';
-
-describe('Embed group getters', () => {
- describe('metricsWithData', () => {
- it('correctly sums the number of metrics with data', () => {
- const mockMetric = {};
- const state = {
- modules: mockNamespaces,
- };
- const rootGetters = {
- [`${mockNamespaces[0]}/metricsWithData`]: () => [mockMetric],
- [`${mockNamespaces[1]}/metricsWithData`]: () => [mockMetric, mockMetric],
- };
-
- expect(metricsWithData(state, null, null, rootGetters)).toEqual([1, 2]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/embed_group/mutations_spec.js b/spec/frontend/monitoring/store/embed_group/mutations_spec.js
deleted file mode 100644
index 2f8d7687aad..00000000000
--- a/spec/frontend/monitoring/store/embed_group/mutations_spec.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as types from '~/monitoring/stores/embed_group/mutation_types';
-import mutations from '~/monitoring/stores/embed_group/mutations';
-import state from '~/monitoring/stores/embed_group/state';
-import { mockNamespace } from '../../mock_data';
-
-describe('Embed group mutations', () => {
- describe('ADD_MODULE', () => {
- it('should add a module', () => {
- const stateCopy = state();
-
- mutations[types.ADD_MODULE](stateCopy, mockNamespace);
-
- expect(stateCopy.modules).toEqual([mockNamespace]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
deleted file mode 100644
index c7f3bdbf1f8..00000000000
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ /dev/null
@@ -1,457 +0,0 @@
-import _ from 'lodash';
-import { metricStates } from '~/monitoring/constants';
-import * as getters from '~/monitoring/stores/getters';
-import * as types from '~/monitoring/stores/mutation_types';
-import mutations from '~/monitoring/stores/mutations';
-import { metricsDashboardPayload } from '../fixture_data';
-import {
- customDashboardBasePath,
- environmentData,
- metricsResult,
- dashboardGitResponse,
- storeVariables,
- mockLinks,
-} from '../mock_data';
-
-describe('Monitoring store Getters', () => {
- let state;
-
- const getMetric = ({ group = 0, panel = 0, metric = 0 } = {}) =>
- state.dashboard.panelGroups[group].panels[panel].metrics[metric];
-
- const setMetricSuccess = ({ group, panel, metric, result = metricsResult } = {}) => {
- const { metricId } = getMetric({ group, panel, metric });
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, {
- metricId,
- data: {
- resultType: 'matrix',
- result,
- },
- });
- };
-
- const setMetricFailure = ({ group, panel, metric } = {}) => {
- const { metricId } = getMetric({ group, panel, metric });
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
- metricId,
- });
- };
-
- describe('getMetricStates', () => {
- let setupState;
- let getMetricStates;
-
- beforeEach(() => {
- setupState = (initState = {}) => {
- state = initState;
- getMetricStates = getters.getMetricStates(state);
- };
- });
-
- it('has method-style access', () => {
- setupState();
-
- expect(getMetricStates).toEqual(expect.any(Function));
- });
-
- it('when dashboard has no panel groups, returns empty', () => {
- setupState({
- dashboard: {
- panelGroups: [],
- },
- });
-
- expect(getMetricStates()).toEqual([]);
- });
-
- describe('when the dashboard is set', () => {
- let groups;
- beforeEach(() => {
- setupState({
- dashboard: { panelGroups: [] },
- });
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- groups = state.dashboard.panelGroups;
- });
-
- it('no loaded metric returns empty', () => {
- expect(getMetricStates()).toEqual([]);
- });
-
- it('on an empty metric with no result, returns NO_DATA', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ group: 2, result: [] });
-
- expect(getMetricStates()).toEqual([metricStates.NO_DATA]);
- });
-
- it('on a metric with a result, returns OK', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ group: 1 });
-
- expect(getMetricStates()).toEqual([metricStates.OK]);
- });
-
- it('on a metric with an error, returns an error', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricFailure({});
-
- expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
- });
-
- it('on multiple metrics with results, returns OK', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- setMetricSuccess({ group: 1 });
- setMetricSuccess({ group: 1, panel: 1 });
-
- expect(getMetricStates()).toEqual([metricStates.OK]);
-
- // Filtered by groups
- expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([metricStates.OK]);
- expect(getMetricStates(state.dashboard.panelGroups[2].key)).toEqual([]);
- });
- it('on multiple metrics errors', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- setMetricFailure({});
- setMetricFailure({ group: 1 });
-
- // Entire dashboard fails
- expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]);
- });
-
- it('on multiple metrics with errors', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- // An success in 1 group
- setMetricSuccess({ group: 1 });
-
- // An error in 2 groups
- setMetricFailure({ group: 1, panel: 1 });
- setMetricFailure({ group: 2, panel: 0 });
-
- expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[1].key)).toEqual([
- metricStates.OK,
- metricStates.UNKNOWN_ERROR,
- ]);
- expect(getMetricStates(groups[2].key)).toEqual([metricStates.UNKNOWN_ERROR]);
- });
- });
- });
-
- describe('metricsWithData', () => {
- let metricsWithData;
- let setupState;
-
- beforeEach(() => {
- setupState = (initState = {}) => {
- state = initState;
- metricsWithData = getters.metricsWithData(state);
- };
- });
-
- afterEach(() => {
- state = null;
- });
-
- it('has method-style access', () => {
- setupState();
-
- expect(metricsWithData).toEqual(expect.any(Function));
- });
-
- it('when dashboard has no panel groups, returns empty', () => {
- setupState({
- dashboard: {
- panelGroups: [],
- },
- });
-
- expect(metricsWithData()).toEqual([]);
- });
-
- describe('when the dashboard is set', () => {
- beforeEach(() => {
- setupState({
- dashboard: { panelGroups: [] },
- });
- });
-
- it('no loaded metric returns empty', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- expect(metricsWithData()).toEqual([]);
- });
-
- it('an empty metric, returns empty', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ result: [] });
-
- expect(metricsWithData()).toEqual([]);
- });
-
- it('a metric with results, it returns a metric', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess();
-
- expect(metricsWithData()).toEqual([getMetric().metricId]);
- });
-
- it('multiple metrics with results, it return multiple metrics', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ panel: 0 });
- setMetricSuccess({ panel: 1 });
-
- expect(metricsWithData()).toEqual([
- getMetric({ panel: 0 }).metricId,
- getMetric({ panel: 1 }).metricId,
- ]);
- });
-
- it('multiple metrics with results, it returns metrics filtered by group', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- setMetricSuccess({ group: 1 });
- setMetricSuccess({ group: 1, panel: 1 });
-
- // First group has metrics
- expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([
- getMetric({ group: 1 }).metricId,
- getMetric({ group: 1, panel: 1 }).metricId,
- ]);
-
- // Second group has no metrics
- expect(metricsWithData(state.dashboard.panelGroups[2].key)).toEqual([]);
- });
- });
- });
-
- describe('filteredEnvironments', () => {
- const setupState = (initState = {}) => {
- state = {
- ...state,
- ...initState,
- };
- };
-
- beforeAll(() => {
- setupState({
- environments: environmentData,
- });
- });
-
- afterAll(() => {
- state = null;
- });
-
- [
- {
- input: '',
- output: 17,
- },
- {
- input: ' ',
- output: 17,
- },
- {
- input: null,
- output: 17,
- },
- {
- input: 'does-not-exist',
- output: 0,
- },
- {
- input: 'noop-branch-',
- output: 15,
- },
- {
- input: 'noop-branch-9',
- output: 1,
- },
- ].forEach(({ input, output }) => {
- it(`filteredEnvironments returns ${output} items for ${input}`, () => {
- setupState({
- environmentsSearchTerm: input,
- });
- expect(getters.filteredEnvironments(state).length).toBe(output);
- });
- });
- });
-
- describe('metricsSavedToDb', () => {
- let metricsSavedToDb;
- let mockData;
-
- beforeEach(() => {
- mockData = _.cloneDeep(metricsDashboardPayload);
- state = {
- dashboard: {
- panelGroups: [],
- },
- };
- });
-
- it('return no metrics when dashboard is not persisted', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData);
- metricsSavedToDb = getters.metricsSavedToDb(state);
-
- expect(metricsSavedToDb).toEqual([]);
- });
-
- it('return a metric id when one metric is persisted', () => {
- const id = 99;
-
- const [metric] = mockData.panel_groups[0].panels[0].metrics;
-
- metric.metric_id = id;
-
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData);
- metricsSavedToDb = getters.metricsSavedToDb(state);
-
- expect(metricsSavedToDb).toEqual([`${id}_${metric.id}`]);
- });
-
- it('return a metric id when two metrics are persisted', () => {
- const id1 = 101;
- const id2 = 102;
-
- const [metric1] = mockData.panel_groups[0].panels[0].metrics;
- const [metric2] = mockData.panel_groups[0].panels[1].metrics;
-
- // database persisted 2 metrics
- metric1.metric_id = id1;
- metric2.metric_id = id2;
-
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData);
- metricsSavedToDb = getters.metricsSavedToDb(state);
-
- expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
- });
- });
-
- describe('getCustomVariablesParams', () => {
- beforeEach(() => {
- state = {
- variables: {},
- };
- });
-
- it('transforms the variables object to an array in the [variable, variable_value] format for all variable types', () => {
- state.variables = storeVariables;
- const variablesArray = getters.getCustomVariablesParams(state);
-
- expect(variablesArray).toEqual({
- 'variables[textSimple]': 'My default value',
- 'variables[textAdvanced]': 'A default value',
- 'variables[customSimple]': 'value1',
- 'variables[customAdvanced]': 'value2',
- 'variables[customAdvancedWithoutLabel]': 'value2',
- 'variables[customAdvancedWithoutOptText]': 'value2',
- });
- });
-
- it('transforms the variables object to an empty array when no keys are present', () => {
- state.variables = [];
- const variablesArray = getters.getCustomVariablesParams(state);
-
- expect(variablesArray).toEqual({});
- });
- });
-
- describe('selectedDashboard', () => {
- const { selectedDashboard } = getters;
- const localGetters = (localState) => ({
- fullDashboardPath: getters.fullDashboardPath(localState),
- });
-
- it('returns a dashboard', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: dashboardGitResponse[0].path,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[0],
- );
- });
-
- it('returns a dashboard different from the overview dashboard', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: dashboardGitResponse[1].path,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[1],
- );
- });
-
- it('returns the overview dashboard when no dashboard is selected', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: null,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[0],
- );
- });
-
- it('returns the overview dashboard when dashboard cannot be found', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: 'wrong_path',
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[0],
- );
- });
-
- it('returns null when no dashboards are present', () => {
- const localState = {
- allDashboards: [],
- currentDashboard: dashboardGitResponse[0].path,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(null);
- });
- });
-
- describe('linksWithMetadata', () => {
- const setupState = (initState = {}) => {
- state = {
- ...state,
- ...initState,
- };
- };
-
- beforeAll(() => {
- setupState({
- links: mockLinks,
- });
- });
-
- afterAll(() => {
- state = null;
- });
-
- it.each`
- timeRange | output
- ${{}} | ${''}
- ${{ start: '2020-01-01T00:00:00.000Z', end: '2020-01-31T23:59:00.000Z' }} | ${'start=2020-01-01T00%3A00%3A00.000Z&end=2020-01-31T23%3A59%3A00.000Z'}
- ${{ duration: { seconds: 86400 } }} | ${'duration_seconds=86400'}
- `('linksWithMetadata returns URLs with time range', ({ timeRange, output }) => {
- setupState({ timeRange });
- const links = getters.linksWithMetadata(state);
- links.forEach(({ url }) => {
- expect(url).toMatch(output);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/index_spec.js b/spec/frontend/monitoring/store/index_spec.js
deleted file mode 100644
index 4184687eec8..00000000000
--- a/spec/frontend/monitoring/store/index_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createStore } from '~/monitoring/stores';
-
-describe('Monitoring Store Index', () => {
- it('creates store with a `monitoringDashboard` namespace', () => {
- expect(createStore().state).toEqual({
- monitoringDashboard: expect.any(Object),
- });
- });
-
- it('creates store with initial values', () => {
- const defaults = {
- deploymentsEndpoint: '/mock/deployments',
- dashboardEndpoint: '/mock/dashboard',
- dashboardsEndpoint: '/mock/dashboards',
- };
-
- const { state } = createStore(defaults);
-
- expect(state).toEqual({
- monitoringDashboard: expect.objectContaining(defaults),
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
deleted file mode 100644
index 3baef743f42..00000000000
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ /dev/null
@@ -1,586 +0,0 @@
-import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
-import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-import * as types from '~/monitoring/stores/mutation_types';
-import mutations from '~/monitoring/stores/mutations';
-import state from '~/monitoring/stores/state';
-import { metricsDashboardPayload } from '../fixture_data';
-import { prometheusMatrixMultiResult } from '../graph_data';
-import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
-
-describe('Monitoring mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('REQUEST_METRICS_DASHBOARD', () => {
- it('sets an empty loading state', () => {
- mutations[types.REQUEST_METRICS_DASHBOARD](stateCopy);
-
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.LOADING);
- });
- });
-
- describe('RECEIVE_METRICS_DASHBOARD_SUCCESS', () => {
- let payload;
- const getGroups = () => stateCopy.dashboard.panelGroups;
-
- beforeEach(() => {
- stateCopy.dashboard.panelGroups = [];
- payload = metricsDashboardPayload;
- });
- it('sets an empty noData state when the dashboard is empty', () => {
- const emptyDashboardPayload = {
- ...payload,
- panel_groups: [],
- };
-
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, emptyDashboardPayload);
- const groups = getGroups();
-
- expect(groups).toEqual([]);
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.NO_DATA);
- });
- it('adds a key to the group', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
- const groups = getGroups();
-
- expect(groups[0].key).toBe('system-metrics-kubernetes-0');
- expect(groups[1].key).toBe('response-metrics-nginx-ingress-vts-1');
- expect(groups[2].key).toBe('response-metrics-nginx-ingress-2');
- });
- it('normalizes values', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
- const expectedLabel = 'Pod average (MB)';
-
- const { label, queryRange } = getGroups()[0].panels[2].metrics[0];
- expect(label).toEqual(expectedLabel);
- expect(queryRange.length).toBeGreaterThan(0);
- });
- it('contains six groups, with panels with a metric each', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
-
- const groups = getGroups();
-
- expect(groups).toBeDefined();
- expect(groups).toHaveLength(6);
-
- expect(groups[0].panels).toHaveLength(7);
- expect(groups[0].panels[0].metrics).toHaveLength(1);
- expect(groups[0].panels[1].metrics).toHaveLength(1);
- expect(groups[0].panels[2].metrics).toHaveLength(1);
-
- expect(groups[1].panels).toHaveLength(3);
- expect(groups[1].panels[0].metrics).toHaveLength(1);
- });
- it('assigns metrics a metric id', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
-
- const groups = getGroups();
-
- expect(groups[0].panels[0].metrics[0].metricId).toEqual(
- 'NO_DB_system_metrics_kubernetes_container_memory_total',
- );
- expect(groups[1].panels[0].metrics[0].metricId).toEqual(
- 'NO_DB_response_metrics_nginx_ingress_throughput_status_code',
- );
- expect(groups[2].panels[0].metrics[0].metricId).toEqual(
- 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
- );
- });
- });
-
- describe('RECEIVE_METRICS_DASHBOARD_FAILURE', () => {
- it('sets an empty noData state when an empty error occurs', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_FAILURE](stateCopy);
-
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.NO_DATA);
- });
-
- it('sets an empty unableToConnect state when an error occurs', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_FAILURE](stateCopy, 'myerror');
-
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.UNABLE_TO_CONNECT);
- });
- });
-
- describe('Dashboard starring mutations', () => {
- it('REQUEST_DASHBOARD_STARRING', () => {
- stateCopy = { isUpdatingStarredValue: false };
- mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
-
- expect(stateCopy.isUpdatingStarredValue).toBe(true);
- });
-
- describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
- let allDashboards;
-
- beforeEach(() => {
- allDashboards = [...dashboardGitResponse];
- stateCopy = {
- allDashboards,
- currentDashboard: allDashboards[1].path,
- isUpdatingStarredValue: true,
- };
- });
-
- it('sets a dashboard as starred', () => {
- mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, {
- selectedDashboard: stateCopy.allDashboards[1],
- newStarredValue: true,
- });
-
- expect(stateCopy.isUpdatingStarredValue).toBe(false);
- expect(stateCopy.allDashboards[1].starred).toBe(true);
- });
-
- it('sets a dashboard as unstarred', () => {
- mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, {
- selectedDashboard: stateCopy.allDashboards[1],
- newStarredValue: false,
- });
-
- expect(stateCopy.isUpdatingStarredValue).toBe(false);
- expect(stateCopy.allDashboards[1].starred).toBe(false);
- });
- });
-
- it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
- stateCopy = { isUpdatingStarredValue: true };
- mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
-
- expect(stateCopy.isUpdatingStarredValue).toBe(false);
- });
- });
-
- describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
- it('stores the deployment data', () => {
- stateCopy.deploymentData = [];
- mutations[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](stateCopy, deploymentData);
- expect(stateCopy.deploymentData).toBeDefined();
- expect(stateCopy.deploymentData).toHaveLength(3);
- expect(typeof stateCopy.deploymentData[0]).toEqual('object');
- });
- });
-
- describe('SET_INITIAL_STATE', () => {
- it('should set all the endpoints', () => {
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- deploymentsEndpoint: 'deployments.json',
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- currentEnvironmentName: 'production',
- });
- expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
- expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
- expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
- expect(stateCopy.currentEnvironmentName).toEqual('production');
- });
-
- it('should not remove previously set properties', () => {
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- });
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- projectPath: '/gitlab-org/gitlab-foss',
- });
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- currentEnvironmentName: 'canary',
- });
-
- expect(stateCopy).toMatchObject({
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- currentEnvironmentName: 'canary',
- });
- });
-
- it('should not update unknown properties', () => {
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- someOtherProperty: 'some invalid value', // someOtherProperty is not allowed
- });
-
- expect(stateCopy.dashboardEndpoint).toBe('dashboard.json');
- expect(stateCopy.someOtherProperty).toBeUndefined();
- });
- });
-
- describe('SET_ENDPOINTS', () => {
- it('should set all the endpoints', () => {
- mutations[types.SET_ENDPOINTS](stateCopy, {
- deploymentsEndpoint: 'deployments.json',
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- });
- expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
- expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
- expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
- });
-
- it('should not remove previously set properties', () => {
- mutations[types.SET_ENDPOINTS](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- });
- mutations[types.SET_ENDPOINTS](stateCopy, {
- projectPath: '/gitlab-org/gitlab-foss',
- });
-
- expect(stateCopy).toMatchObject({
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- });
- });
-
- it('should not update unknown properties', () => {
- mutations[types.SET_ENDPOINTS](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- someOtherProperty: 'some invalid value', // someOtherProperty is not allowed
- });
-
- expect(stateCopy.dashboardEndpoint).toBe('dashboard.json');
- expect(stateCopy.someOtherProperty).toBeUndefined();
- });
- });
-
- describe('Individual panel/metric results', () => {
- const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
-
- const dashboard = metricsDashboardPayload;
- const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0];
-
- describe('REQUEST_METRIC_RESULT', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
- });
- it('stores a loading state on a metric', () => {
- mutations[types.REQUEST_METRIC_RESULT](stateCopy, {
- metricId,
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: true,
- }),
- );
- });
- });
-
- describe('RECEIVE_METRIC_RESULT_SUCCESS', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
- });
-
- it('adds results to the store', () => {
- const data = prometheusMatrixMultiResult();
-
- expect(getMetric().result).toBe(null);
-
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
- metricId,
- data,
- });
-
- expect(getMetric().result).toHaveLength(data.result.length);
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- state: metricStates.OK,
- }),
- );
- });
- });
-
- describe('RECEIVE_METRIC_RESULT_FAILURE', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
- });
-
- it('stores a timeout error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: { message: 'BACKOFF_TIMEOUT' },
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.TIMEOUT,
- }),
- );
- });
-
- it('stores a connection failed error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: {
- response: {
- status: HTTP_STATUS_SERVICE_UNAVAILABLE,
- },
- },
- });
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.CONNECTION_FAILED,
- }),
- );
- });
-
- it('stores a bad data error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: {
- response: {
- status: HTTP_STATUS_BAD_REQUEST,
- },
- },
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.BAD_QUERY,
- }),
- );
- });
-
- it('stores an unknown error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: null, // no reason in response
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.UNKNOWN_ERROR,
- }),
- );
- });
- });
- });
-
- describe('SET_ALL_DASHBOARDS', () => {
- it('stores `undefined` dashboards as an empty array', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined);
-
- expect(stateCopy.allDashboards).toEqual([]);
- });
-
- it('stores `null` dashboards as an empty array', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, null);
-
- expect(stateCopy.allDashboards).toEqual([]);
- });
-
- it('stores dashboards loaded from the git repository', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
- expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
- });
- });
-
- describe('SET_EXPANDED_PANEL', () => {
- it('no expanded panel is set initally', () => {
- expect(stateCopy.expandedPanel.panel).toEqual(null);
- expect(stateCopy.expandedPanel.group).toEqual(null);
- });
-
- it('sets a panel id as the expanded panel', () => {
- const group = 'group_1';
- const panel = { title: 'A Panel' };
- mutations[types.SET_EXPANDED_PANEL](stateCopy, { group, panel });
-
- expect(stateCopy.expandedPanel).toEqual({ group, panel });
- });
-
- it('clears panel as the expanded panel', () => {
- mutations[types.SET_EXPANDED_PANEL](stateCopy, { group: null, panel: null });
-
- expect(stateCopy.expandedPanel.group).toEqual(null);
- expect(stateCopy.expandedPanel.panel).toEqual(null);
- });
- });
-
- describe('UPDATE_VARIABLE_VALUE', () => {
- it('updates only the value of the variable in variables', () => {
- stateCopy.variables = storeTextVariables;
- mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { name: 'textSimple', value: 'New Value' });
-
- expect(stateCopy.variables[0].value).toEqual('New Value');
- });
- });
-
- describe('UPDATE_VARIABLE_METRIC_LABEL_VALUES', () => {
- it('updates options in a variable', () => {
- const data = [
- {
- __name__: 'up',
- job: 'prometheus',
- env: 'prd',
- },
- {
- __name__: 'up',
- job: 'prometheus',
- env: 'stg',
- },
- {
- __name__: 'up',
- job: 'node',
- env: 'prod',
- },
- {
- __name__: 'up',
- job: 'node',
- env: 'stg',
- },
- ];
-
- const variable = {
- options: {},
- };
-
- mutations[types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](stateCopy, {
- variable,
- label: 'job',
- data,
- });
-
- expect(variable.options).toEqual({
- values: [
- { text: 'prometheus', value: 'prometheus' },
- { text: 'node', value: 'node' },
- ],
- });
- });
- });
-
- describe('REQUEST_PANEL_PREVIEW', () => {
- it('saves yml content and resets other preview data', () => {
- const mockYmlContent = 'mock yml content';
- mutations[types.REQUEST_PANEL_PREVIEW](stateCopy, mockYmlContent);
-
- expect(stateCopy.panelPreviewIsLoading).toBe(true);
- expect(stateCopy.panelPreviewYml).toBe(mockYmlContent);
- expect(stateCopy.panelPreviewGraphData).toBe(null);
- expect(stateCopy.panelPreviewError).toBe(null);
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_SUCCESS', () => {
- it('saves graph data', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_SUCCESS](stateCopy, {
- title: 'My Title',
- type: 'area-chart',
- });
-
- expect(stateCopy.panelPreviewIsLoading).toBe(false);
- expect(stateCopy.panelPreviewGraphData).toMatchObject({
- title: 'My Title',
- type: 'area-chart',
- });
- expect(stateCopy.panelPreviewError).toBe(null);
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_FAILURE', () => {
- it('saves graph data', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_FAILURE](stateCopy, 'Error!');
-
- expect(stateCopy.panelPreviewIsLoading).toBe(false);
- expect(stateCopy.panelPreviewGraphData).toBe(null);
- expect(stateCopy.panelPreviewError).toBe('Error!');
- });
- });
-
- describe('panel preview metric', () => {
- const getPreviewMetricAt = (i) => stateCopy.panelPreviewGraphData.metrics[i];
-
- beforeEach(() => {
- stateCopy.panelPreviewGraphData = {
- title: 'Preview panel title',
- metrics: [
- {
- query: 'query',
- },
- ],
- };
- });
-
- describe('REQUEST_PANEL_PREVIEW_METRIC_RESULT', () => {
- it('sets the metric to loading for the first time', () => {
- mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
-
- expect(getPreviewMetricAt(0).loading).toBe(true);
- expect(getPreviewMetricAt(0).state).toBe(metricStates.LOADING);
- });
-
- it('sets the metric to loading and keeps the result', () => {
- getPreviewMetricAt(0).result = [[0, 1]];
- getPreviewMetricAt(0).state = metricStates.OK;
-
- mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: true,
- result: [[0, 1]],
- state: metricStates.OK,
- });
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS', () => {
- it('saves the result in the metric', () => {
- const data = prometheusMatrixMultiResult();
-
- mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](stateCopy, {
- index: 0,
- data,
- });
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: false,
- state: metricStates.OK,
- result: expect.any(Array),
- });
- expect(getPreviewMetricAt(0).result).toHaveLength(data.result.length);
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE', () => {
- it('stores an error in the metric', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
- index: 0,
- });
-
- expect(getPreviewMetricAt(0).loading).toBe(false);
- expect(getPreviewMetricAt(0).state).toBe(metricStates.UNKNOWN_ERROR);
- expect(getPreviewMetricAt(0).result).toBe(null);
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: false,
- result: null,
- state: metricStates.UNKNOWN_ERROR,
- });
- });
-
- it('stores a timeout error in a metric', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
- index: 0,
- error: { message: 'BACKOFF_TIMEOUT' },
- });
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: false,
- result: null,
- state: metricStates.TIMEOUT,
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
deleted file mode 100644
index 54f9c59308e..00000000000
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ /dev/null
@@ -1,893 +0,0 @@
-import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
-import * as urlUtils from '~/lib/utils/url_utility';
-import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
-import {
- uniqMetricsId,
- parseEnvironmentsResponse,
- parseAnnotationsResponse,
- removeLeadingSlash,
- mapToDashboardViewModel,
- normalizeQueryResponseData,
- convertToGrafanaTimeRange,
- addDashboardMetaDataToLink,
- normalizeCustomDashboardPath,
-} from '~/monitoring/stores/utils';
-import { annotationsData } from '../mock_data';
-
-const projectPath = 'gitlab-org/gitlab-test';
-
-describe('mapToDashboardViewModel', () => {
- it('maps an empty dashboard', () => {
- expect(mapToDashboardViewModel({})).toEqual({
- dashboard: '',
- panelGroups: [],
- links: [],
- variables: [],
- });
- });
-
- it('maps a simple dashboard', () => {
- const response = {
- dashboard: 'Dashboard Name',
- panel_groups: [
- {
- group: 'Group 1',
- panels: [
- {
- id: 'ID_ABC',
- title: 'Title A',
- xLabel: '',
- xAxis: {
- name: '',
- },
- type: 'chart-type',
- y_label: 'Y Label A',
- metrics: [],
- },
- ],
- },
- ],
- };
-
- expect(mapToDashboardViewModel(response)).toEqual({
- dashboard: 'Dashboard Name',
- links: [],
- variables: [],
- panelGroups: [
- {
- group: 'Group 1',
- key: 'group-1-0',
- panels: [
- {
- id: 'ID_ABC',
- title: 'Title A',
- type: 'chart-type',
- xLabel: '',
- xAxis: {
- name: '',
- },
- y_label: 'Y Label A',
- yAxis: {
- name: 'Y Label A',
- format: 'engineering',
- precision: 2,
- },
- links: [],
- metrics: [],
- },
- ],
- },
- ],
- });
- });
-
- describe('panel groups mapping', () => {
- it('key', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- variables: {},
- panel_groups: [
- {
- group: 'Group A',
- },
- {
- group: 'Group B',
- },
- {
- group: '',
- unsupported_property: 'This should be removed',
- },
- ],
- };
-
- expect(mapToDashboardViewModel(response).panelGroups).toEqual([
- {
- group: 'Group A',
- key: 'group-a-0',
- panels: [],
- },
- {
- group: 'Group B',
- key: 'group-b-1',
- panels: [],
- },
- {
- group: '',
- key: 'default-2',
- panels: [],
- },
- ]);
- });
- });
-
- describe('panel mapping', () => {
- const panelTitle = 'Panel Title';
- const yAxisName = 'Y Axis Name';
-
- let dashboard;
-
- const setupWithPanel = (panel) => {
- dashboard = {
- panel_groups: [
- {
- panels: [panel],
- },
- ],
- };
- };
-
- const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0];
-
- it('panel with x_label', () => {
- setupWithPanel({
- id: 'ID_123',
- title: panelTitle,
- x_label: 'x label',
- });
-
- expect(getMappedPanel()).toEqual({
- id: 'ID_123',
- title: panelTitle,
- xLabel: 'x label',
- xAxis: {
- name: 'x label',
- },
- y_label: '',
- yAxis: {
- name: '',
- format: SUPPORTED_FORMATS.engineering,
- precision: 2,
- },
- links: [],
- metrics: [],
- });
- });
-
- it('group y_axis defaults', () => {
- setupWithPanel({
- id: 'ID_456',
- title: panelTitle,
- });
-
- expect(getMappedPanel()).toEqual({
- id: 'ID_456',
- title: panelTitle,
- xLabel: '',
- y_label: '',
- xAxis: {
- name: '',
- },
- yAxis: {
- name: '',
- format: SUPPORTED_FORMATS.engineering,
- precision: 2,
- },
- links: [],
- metrics: [],
- });
- });
-
- it('panel with y_axis.name', () => {
- setupWithPanel({
- y_axis: {
- name: yAxisName,
- },
- });
-
- expect(getMappedPanel().y_label).toBe(yAxisName);
- expect(getMappedPanel().yAxis.name).toBe(yAxisName);
- });
-
- it('panel with y_axis.name and y_label, displays y_axis.name', () => {
- setupWithPanel({
- y_label: 'Ignored Y Label',
- y_axis: {
- name: yAxisName,
- },
- });
-
- expect(getMappedPanel().y_label).toBe(yAxisName);
- expect(getMappedPanel().yAxis.name).toBe(yAxisName);
- });
-
- it('group y_label', () => {
- setupWithPanel({
- y_label: yAxisName,
- });
-
- expect(getMappedPanel().y_label).toBe(yAxisName);
- expect(getMappedPanel().yAxis.name).toBe(yAxisName);
- });
-
- it('group y_axis format and precision', () => {
- setupWithPanel({
- title: panelTitle,
- y_axis: {
- precision: 0,
- format: SUPPORTED_FORMATS.bytes,
- },
- });
-
- expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.bytes);
- expect(getMappedPanel().yAxis.precision).toBe(0);
- });
-
- it('group y_axis unsupported format defaults to number', () => {
- setupWithPanel({
- title: panelTitle,
- y_axis: {
- format: 'invalid_format',
- },
- });
-
- expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.engineering);
- });
-
- // This property allows single_stat panels to render percentile values
- it('group maxValue', () => {
- setupWithPanel({
- max_value: 100,
- });
-
- expect(getMappedPanel().maxValue).toBe(100);
- });
-
- describe('panel with links', () => {
- const title = 'Example';
- const url = 'https://example.com';
-
- it('maps an empty link collection', () => {
- setupWithPanel({
- links: undefined,
- });
-
- expect(getMappedPanel().links).toEqual([]);
- });
-
- it('maps a link', () => {
- setupWithPanel({ links: [{ title, url }] });
-
- expect(getMappedPanel().links).toEqual([{ title, url }]);
- });
-
- it('maps a link without a title', () => {
- setupWithPanel({
- links: [{ url }],
- });
-
- expect(getMappedPanel().links).toEqual([{ title: url, url }]);
- });
-
- it('maps a link without a url', () => {
- setupWithPanel({
- links: [{ title }],
- });
-
- expect(getMappedPanel().links).toEqual([{ title, url: '#' }]);
- });
-
- it('maps a link without a url or title', () => {
- setupWithPanel({
- links: [{}],
- });
-
- expect(getMappedPanel().links).toEqual([{ title: 'null', url: '#' }]);
- });
-
- it('maps a link with an unsafe url safely', () => {
- // eslint-disable-next-line no-script-url
- const unsafeUrl = 'javascript:alert("XSS")';
-
- setupWithPanel({
- links: [
- {
- title,
- url: unsafeUrl,
- },
- ],
- });
-
- expect(getMappedPanel().links).toEqual([{ title, url: '#' }]);
- });
-
- it('maps multple links', () => {
- setupWithPanel({
- links: [{ title, url }, { url }, { title }],
- });
-
- expect(getMappedPanel().links).toEqual([
- { title, url },
- { title: url, url },
- { title, url: '#' },
- ]);
- });
- });
- });
-
- describe('metrics mapping', () => {
- const defaultLabel = 'Panel Label';
- const dashboardWithMetric = (metric, label = defaultLabel) => ({
- panel_groups: [
- {
- panels: [
- {
- y_label: label,
- metrics: [metric],
- },
- ],
- },
- ],
- });
-
- const getMappedMetric = (dashboard) => {
- return mapToDashboardViewModel(dashboard).panelGroups[0].panels[0].metrics[0];
- };
-
- it('creates a metric', () => {
- const dashboard = dashboardWithMetric({ label: 'Panel Label' });
-
- expect(getMappedMetric(dashboard)).toEqual({
- label: expect.any(String),
- metricId: expect.any(String),
- loading: false,
- result: null,
- state: null,
- });
- });
-
- it('creates a metric with a correct id', () => {
- const dashboard = dashboardWithMetric({
- id: 'http_responses',
- metric_id: 1,
- });
-
- expect(getMappedMetric(dashboard).metricId).toEqual('1_http_responses');
- });
-
- it('creates a metric without a default label', () => {
- const dashboard = dashboardWithMetric({});
-
- expect(getMappedMetric(dashboard)).toMatchObject({
- label: undefined,
- });
- });
-
- it('creates a metric with an endpoint and query', () => {
- const dashboard = dashboardWithMetric({
- prometheus_endpoint_path: 'http://test',
- query_range: 'http_responses',
- });
-
- expect(getMappedMetric(dashboard)).toMatchObject({
- prometheusEndpointPath: 'http://test',
- queryRange: 'http_responses',
- });
- });
-
- it('creates a metric with an ad-hoc property', () => {
- // This behavior is deprecated and should be removed
- // https://gitlab.com/gitlab-org/gitlab/issues/207198
-
- const dashboard = dashboardWithMetric({
- x_label: 'Another label',
- unkown_option: 'unkown_data',
- });
-
- expect(getMappedMetric(dashboard)).toMatchObject({
- x_label: 'Another label',
- unkown_option: 'unkown_data',
- });
- });
- });
-
- describe('templating variables mapping', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- it('sets variables as-is from yml file if URL has no variables', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- templating: {
- variables: {
- pod: 'kubernetes',
- pod_2: 'kubernetes-2',
- },
- },
- };
-
- urlUtils.queryToObject.mockReturnValueOnce();
-
- expect(mapToDashboardViewModel(response).variables).toEqual([
- {
- name: 'pod',
- label: 'pod',
- type: 'text',
- value: 'kubernetes',
- },
- {
- name: 'pod_2',
- label: 'pod_2',
- type: 'text',
- value: 'kubernetes-2',
- },
- ]);
- });
-
- it('sets variables as-is from yml file if URL has no matching variables', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- templating: {
- variables: {
- pod: 'kubernetes',
- pod_2: 'kubernetes-2',
- },
- },
- };
-
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-environment': 'POD',
- });
-
- expect(mapToDashboardViewModel(response).variables).toEqual([
- {
- label: 'pod',
- name: 'pod',
- type: 'text',
- value: 'kubernetes',
- },
- {
- label: 'pod_2',
- name: 'pod_2',
- type: 'text',
- value: 'kubernetes-2',
- },
- ]);
- });
-
- it('merges variables from URL with the ones from yml file', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- templating: {
- variables: {
- pod: 'kubernetes',
- pod_2: 'kubernetes-2',
- },
- },
- };
-
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-environment': 'POD',
- 'var-pod': 'POD1',
- 'var-pod_2': 'POD2',
- });
-
- expect(mapToDashboardViewModel(response).variables).toEqual([
- {
- label: 'pod',
- name: 'pod',
- type: 'text',
- value: 'POD1',
- },
- {
- label: 'pod_2',
- name: 'pod_2',
- type: 'text',
- value: 'POD2',
- },
- ]);
- });
- });
-});
-
-describe('uniqMetricsId', () => {
- [
- { input: { id: 1 }, expected: `${NOT_IN_DB_PREFIX}_1` },
- { input: { metricId: 2 }, expected: '2_undefined' },
- { input: { metricId: 2, id: 21 }, expected: '2_21' },
- { input: { metricId: 22, id: 1 }, expected: '22_1' },
- { input: { metricId: 'aaa', id: '_a' }, expected: 'aaa__a' },
- ].forEach(({ input, expected }) => {
- it(`creates unique metric ID with ${JSON.stringify(input)}`, () => {
- expect(uniqMetricsId(input)).toEqual(expected);
- });
- });
-});
-
-describe('parseEnvironmentsResponse', () => {
- [
- {
- input: null,
- output: [],
- },
- {
- input: undefined,
- output: [],
- },
- {
- input: [],
- output: [],
- },
- {
- input: [
- {
- id: '1',
- name: 'env-1',
- },
- ],
- output: [
- {
- id: 1,
- name: 'env-1',
- metrics_path: `${projectPath}/-/metrics?environment=1`,
- },
- ],
- },
- {
- input: [
- {
- id: 'gid://gitlab/Environment/12',
- name: 'env-12',
- },
- ],
- output: [
- {
- id: 12,
- name: 'env-12',
- metrics_path: `${projectPath}/-/metrics?environment=12`,
- },
- ],
- },
- ].forEach(({ input, output }) => {
- it(`parseEnvironmentsResponse returns ${JSON.stringify(output)} with input ${JSON.stringify(
- input,
- )}`, () => {
- expect(parseEnvironmentsResponse(input, projectPath)).toEqual(output);
- });
- });
-});
-
-describe('parseAnnotationsResponse', () => {
- const parsedAnnotationResponse = [
- {
- description: 'This is a test annotation',
- endingAt: null,
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
- panelId: null,
- startingAt: new Date('2020-04-12T12:51:53.000Z'),
- },
- ];
- it.each`
- case | input | expected
- ${'Returns empty array for null input'} | ${null} | ${[]}
- ${'Returns empty array for undefined input'} | ${undefined} | ${[]}
- ${'Returns empty array for empty input'} | ${[]} | ${[]}
- ${'Returns parsed responses for annotations data'} | ${[annotationsData[0]]} | ${parsedAnnotationResponse}
- `('$case', ({ input, expected }) => {
- expect(parseAnnotationsResponse(input)).toEqual(expected);
- });
-});
-
-describe('removeLeadingSlash', () => {
- [
- { input: null, output: '' },
- { input: '', output: '' },
- { input: 'gitlab-org', output: 'gitlab-org' },
- { input: 'gitlab-org/gitlab', output: 'gitlab-org/gitlab' },
- { input: '/gitlab-org/gitlab', output: 'gitlab-org/gitlab' },
- { input: '////gitlab-org/gitlab', output: 'gitlab-org/gitlab' },
- ].forEach(({ input, output }) => {
- it(`removeLeadingSlash returns ${output} with input ${input}`, () => {
- expect(removeLeadingSlash(input)).toEqual(output);
- });
- });
-});
-
-describe('user-defined links utils', () => {
- const mockRelativeTimeRange = {
- metricsDashboard: {
- duration: {
- seconds: 86400,
- },
- },
- grafana: {
- from: 'now-86400s',
- to: 'now',
- },
- };
- const mockAbsoluteTimeRange = {
- metricsDashboard: {
- start: '2020-06-08T16:13:01.995Z',
- end: '2020-06-08T21:12:32.243Z',
- },
- grafana: {
- from: 1591632781995,
- to: 1591650752243,
- },
- };
- describe('convertToGrafanaTimeRange', () => {
- it('converts relative timezone to grafana timezone', () => {
- expect(convertToGrafanaTimeRange(mockRelativeTimeRange.metricsDashboard)).toEqual(
- mockRelativeTimeRange.grafana,
- );
- });
-
- it('converts absolute timezone to grafana timezone', () => {
- expect(convertToGrafanaTimeRange(mockAbsoluteTimeRange.metricsDashboard)).toEqual(
- mockAbsoluteTimeRange.grafana,
- );
- });
- });
-
- describe('addDashboardMetaDataToLink', () => {
- const link = { title: 'title', url: 'https://gitlab.com' };
- const grafanaLink = { ...link, type: 'grafana' };
-
- it('adds relative time range to link w/o type for metrics dashboards', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockRelativeTimeRange.metricsDashboard,
- });
- expect(adder(link)).toMatchObject({
- title: 'title',
- url: 'https://gitlab.com?duration_seconds=86400',
- });
- });
-
- it('adds relative time range to Grafana type links', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockRelativeTimeRange.metricsDashboard,
- });
- expect(adder(grafanaLink)).toMatchObject({
- title: 'title',
- url: 'https://gitlab.com?from=now-86400s&to=now',
- });
- });
-
- it('adds absolute time range to link w/o type for metrics dashboard', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockAbsoluteTimeRange.metricsDashboard,
- });
- expect(adder(link)).toMatchObject({
- title: 'title',
- url:
- 'https://gitlab.com?start=2020-06-08T16%3A13%3A01.995Z&end=2020-06-08T21%3A12%3A32.243Z',
- });
- });
-
- it('adds absolute time range to Grafana type links', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockAbsoluteTimeRange.metricsDashboard,
- });
- expect(adder(grafanaLink)).toMatchObject({
- title: 'title',
- url: 'https://gitlab.com?from=1591632781995&to=1591650752243',
- });
- });
- });
-});
-
-describe('normalizeQueryResponseData', () => {
- // Data examples from
- // https://prometheus.io/docs/prometheus/latest/querying/api/#expression-queries
-
- it('processes a string result', () => {
- const mockScalar = {
- resultType: 'string',
- result: [1435781451.781, '1'],
- };
-
- expect(normalizeQueryResponseData(mockScalar)).toEqual([
- {
- metric: {},
- value: ['2015-07-01T20:10:51.781Z', '1'],
- values: [['2015-07-01T20:10:51.781Z', '1']],
- },
- ]);
- });
-
- it('processes a scalar result', () => {
- const mockScalar = {
- resultType: 'scalar',
- result: [1435781451.781, '1'],
- };
-
- expect(normalizeQueryResponseData(mockScalar)).toEqual([
- {
- metric: {},
- value: ['2015-07-01T20:10:51.781Z', 1],
- values: [['2015-07-01T20:10:51.781Z', 1]],
- },
- ]);
- });
-
- it('processes a vector result', () => {
- const mockVector = {
- resultType: 'vector',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- value: [1435781451.781, '1'],
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9100',
- },
- value: [1435781451.781, '0'],
- },
- ],
- };
-
- expect(normalizeQueryResponseData(mockVector)).toEqual([
- {
- metric: { __name__: 'up', job: 'prometheus', instance: 'localhost:9090' },
- value: ['2015-07-01T20:10:51.781Z', 1],
- values: [['2015-07-01T20:10:51.781Z', 1]],
- },
- {
- metric: { __name__: 'up', job: 'node', instance: 'localhost:9100' },
- value: ['2015-07-01T20:10:51.781Z', 0],
- values: [['2015-07-01T20:10:51.781Z', 0]],
- },
- ]);
- });
-
- it('processes a matrix result', () => {
- const mockMatrix = {
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [
- [1435781430.781, '1'],
- [1435781445.781, '2'],
- [1435781460.781, '3'],
- ],
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: [
- [1435781430.781, '4'],
- [1435781445.781, '5'],
- [1435781460.781, '6'],
- ],
- },
- ],
- };
-
- expect(normalizeQueryResponseData(mockMatrix)).toEqual([
- {
- metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
- value: ['2015-07-01T20:11:00.781Z', 3],
- values: [
- ['2015-07-01T20:10:30.781Z', 1],
- ['2015-07-01T20:10:45.781Z', 2],
- ['2015-07-01T20:11:00.781Z', 3],
- ],
- },
- {
- metric: { __name__: 'up', instance: 'localhost:9091', job: 'node' },
- value: ['2015-07-01T20:11:00.781Z', 6],
- values: [
- ['2015-07-01T20:10:30.781Z', 4],
- ['2015-07-01T20:10:45.781Z', 5],
- ['2015-07-01T20:11:00.781Z', 6],
- ],
- },
- ]);
- });
-
- it('processes a scalar result with a NaN result', () => {
- // Queries may return "NaN" string values.
- // e.g. when Prometheus cannot find a metric the query
- // `scalar(does_not_exist)` will return a "NaN" value.
-
- const mockScalar = {
- resultType: 'scalar',
- result: [1435781451.781, 'NaN'],
- };
-
- expect(normalizeQueryResponseData(mockScalar)).toEqual([
- {
- metric: {},
- value: ['2015-07-01T20:10:51.781Z', NaN],
- values: [['2015-07-01T20:10:51.781Z', NaN]],
- },
- ]);
- });
-
- it('processes a matrix result with a "NaN" value', () => {
- // Queries may return "NaN" string values.
- const mockMatrix = {
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [
- [1435781430.781, '1'],
- [1435781460.781, 'NaN'],
- ],
- },
- ],
- };
-
- expect(normalizeQueryResponseData(mockMatrix)).toEqual([
- {
- metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
- value: ['2015-07-01T20:11:00.781Z', NaN],
- values: [
- ['2015-07-01T20:10:30.781Z', 1],
- ['2015-07-01T20:11:00.781Z', NaN],
- ],
- },
- ]);
- });
-});
-
-describe('normalizeCustomDashboardPath', () => {
- it.each`
- input | expected
- ${[undefined]} | ${''}
- ${[null]} | ${''}
- ${[]} | ${''}
- ${['links.yml']} | ${'links.yml'}
- ${['links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
- ${['config/prometheus/common_metrics.yml']} | ${'config/prometheus/common_metrics.yml'}
- ${['config/prometheus/common_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/common_metrics.yml'}
- ${['dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
- ${['dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
- ${['.gitlab/dashboards/links.yml']} | ${'.gitlab/dashboards/links.yml'}
- ${['.gitlab/dashboards/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
- ${['.gitlab/dashboards/dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
- ${['.gitlab/dashboards/dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
- ${['config/prometheus/pod_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/pod_metrics.yml'}
- ${['config/prometheus/pod_metrics.yml']} | ${'config/prometheus/pod_metrics.yml'}
- `(`normalizeCustomDashboardPath returns $expected for $input`, ({ input, expected }) => {
- expect(normalizeCustomDashboardPath(...input)).toEqual(expected);
- });
-});
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
deleted file mode 100644
index 58e7175c04c..00000000000
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ /dev/null
@@ -1,209 +0,0 @@
-import * as urlUtils from '~/lib/utils/url_utility';
-import {
- parseTemplatingVariables,
- mergeURLVariables,
- optionsFromSeriesData,
-} from '~/monitoring/stores/variable_mapping';
-import {
- templatingVariablesExamples,
- storeTextVariables,
- storeCustomVariables,
- storeMetricLabelValuesVariables,
-} from '../mock_data';
-
-describe('Monitoring variable mapping', () => {
- describe('parseTemplatingVariables', () => {
- it.each`
- case | input
- ${'For undefined templating object'} | ${undefined}
- ${'For empty templating object'} | ${{}}
- `('$case, returns an empty array', ({ input }) => {
- expect(parseTemplatingVariables(input)).toEqual([]);
- });
-
- it.each`
- case | input | output
- ${'Returns parsed object for text variables'} | ${templatingVariablesExamples.text} | ${storeTextVariables}
- ${'Returns parsed object for custom variables'} | ${templatingVariablesExamples.custom} | ${storeCustomVariables}
- ${'Returns parsed object for metric label value variables'} | ${templatingVariablesExamples.metricLabelValues} | ${storeMetricLabelValuesVariables}
- `('$case, returns an empty array', ({ input, output }) => {
- expect(parseTemplatingVariables(input)).toEqual(output);
- });
- });
-
- describe('mergeURLVariables', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- it('returns empty object if variables are not defined in yml or URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
-
- expect(mergeURLVariables([])).toEqual([]);
- });
-
- it('returns empty object if variables are defined in URL but not in yml', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-env': 'one',
- 'var-instance': 'localhost',
- });
-
- expect(mergeURLVariables([])).toEqual([]);
- });
-
- it('returns yml variables if variables defined in yml but not in the URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
-
- const variables = [
- {
- name: 'env',
- value: 'one',
- },
- {
- name: 'instance',
- value: 'localhost',
- },
- ];
-
- expect(mergeURLVariables(variables)).toEqual(variables);
- });
-
- it('returns yml variables if variables defined in URL do not match with yml variables', () => {
- const urlParams = {
- 'var-env': 'one',
- 'var-instance': 'localhost',
- };
- const variables = [
- {
- name: 'env',
- value: 'one',
- },
- {
- name: 'service',
- value: 'database',
- },
- ];
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
-
- expect(mergeURLVariables(variables)).toEqual(variables);
- });
-
- it('returns merged yml and URL variables if there is some match', () => {
- const urlParams = {
- 'var-env': 'one',
- 'var-instance': 'localhost:8080',
- };
- const variables = [
- {
- name: 'instance',
- value: 'localhost',
- },
- {
- name: 'service',
- value: 'database',
- },
- ];
-
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
-
- expect(mergeURLVariables(variables)).toEqual([
- {
- name: 'instance',
- value: 'localhost:8080',
- },
- {
- name: 'service',
- value: 'database',
- },
- ]);
- });
- });
-
- describe('optionsFromSeriesData', () => {
- it('fetches the label values from missing data', () => {
- expect(optionsFromSeriesData({ label: 'job' })).toEqual([]);
- });
-
- it('fetches the label values from a simple series', () => {
- const data = [
- {
- __name__: 'up',
- job: 'job1',
- },
- {
- __name__: 'up',
- job: 'job2',
- },
- ];
-
- expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
- { text: 'job1', value: 'job1' },
- { text: 'job2', value: 'job2' },
- ]);
- });
-
- it('fetches the label values from multiple series', () => {
- const data = [
- {
- __name__: 'up',
- job: 'job1',
- instance: 'host1',
- },
- {
- __name__: 'up',
- job: 'job2',
- instance: 'host1',
- },
- {
- __name__: 'up',
- job: 'job1',
- instance: 'host2',
- },
- {
- __name__: 'up',
- job: 'job2',
- instance: 'host2',
- },
- ];
-
- expect(optionsFromSeriesData({ label: '__name__', data })).toEqual([
- { text: 'up', value: 'up' },
- ]);
-
- expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
- { text: 'job1', value: 'job1' },
- { text: 'job2', value: 'job2' },
- ]);
-
- expect(optionsFromSeriesData({ label: 'instance', data })).toEqual([
- { text: 'host1', value: 'host1' },
- { text: 'host2', value: 'host2' },
- ]);
- });
-
- it('fetches the label values from a series with missing values', () => {
- const data = [
- {
- __name__: 'up',
- job: 'job1',
- },
- {
- __name__: 'up',
- job: 'job2',
- },
- {
- __name__: 'up',
- },
- ];
-
- expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
- { text: 'job1', value: 'job1' },
- { text: 'job2', value: 'job2' },
- ]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
deleted file mode 100644
index 96219661b9b..00000000000
--- a/spec/frontend/monitoring/store_utils.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import * as types from '~/monitoring/stores/mutation_types';
-import { metricsDashboardPayload } from './fixture_data';
-import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
-
-export const setMetricResult = ({ store, result, group = 0, panel = 0, metric = 0 }) => {
- const { dashboard } = store.state.monitoringDashboard;
- const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric];
-
- store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
- metricId,
- data: {
- resultType: 'matrix',
- result,
- },
- });
-};
-
-const setEnvironmentData = (store) => {
- store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
-};
-
-export const setupAllDashboards = (store, path) => {
- store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
- if (path) {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: path,
- });
- }
-};
-
-export const setupStoreWithDashboard = (store) => {
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
- metricsDashboardPayload,
- );
-};
-
-export const setupStoreWithLinks = (store) => {
- store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, {
- ...metricsDashboardPayload,
- links: [
- {
- title: 'GitLab Website',
- url: `https://gitlab.com/website`,
- },
- ],
- });
-};
-
-export const setupStoreWithData = (store) => {
- setupAllDashboards(store);
- setupStoreWithDashboard(store);
-
- setMetricResult({ store, result: [], panel: 0 });
- setMetricResult({ store, result: metricsResult, panel: 1 });
- setMetricResult({ store, result: metricsResult, panel: 2 });
-
- setEnvironmentData(store);
-};
-
-export const setupStoreWithDataForPanelCount = (store, panelCount) => {
- const payloadPanelGroup = metricsDashboardPayload.panel_groups[0];
-
- const panelGroupCustom = {
- ...payloadPanelGroup,
- panels: payloadPanelGroup.panels.slice(0, panelCount),
- };
-
- const metricsDashboardPayloadCustom = {
- ...metricsDashboardPayload,
- panel_groups: [panelGroupCustom],
- };
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
- metricsDashboardPayloadCustom,
- );
-
- setMetricResult({ store, result: metricsResult, panel: 0 });
-};
diff --git a/spec/frontend/monitoring/stubs/modal_stub.js b/spec/frontend/monitoring/stubs/modal_stub.js
deleted file mode 100644
index 4cd0362096e..00000000000
--- a/spec/frontend/monitoring/stubs/modal_stub.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const ModalStub = {
- name: 'glmodal-stub',
- template: `
- <div>
- <slot></slot>
- <slot name="modal-ok"></slot>
- </div>
- `,
-};
-
-export default ModalStub;
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
deleted file mode 100644
index 348825c334a..00000000000
--- a/spec/frontend/monitoring/utils_spec.js
+++ /dev/null
@@ -1,464 +0,0 @@
-import { TEST_HOST } from 'helpers/test_constants';
-import * as urlUtils from '~/lib/utils/url_utility';
-import * as monitoringUtils from '~/monitoring/utils';
-import { metricsDashboardViewModel, graphData } from './fixture_data';
-import { singleStatGraphData, anomalyGraphData } from './graph_data';
-import { mockProjectDir, barMockData } from './mock_data';
-
-const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
-
-const generatedLink = 'http://chart.link.com';
-
-const chartTitle = 'Some metric chart';
-
-const range = {
- start: '2019-01-01T00:00:00.000Z',
- end: '2019-01-10T00:00:00.000Z',
-};
-
-const rollingRange = {
- duration: { seconds: 120 },
-};
-
-describe('monitoring/utils', () => {
- describe('trackGenerateLinkToChartEventOptions', () => {
- it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
- document.body.dataset.page = 'groups:clusters:show';
-
- expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
- category: 'Cluster Monitoring',
- action: 'generate_link_to_cluster_metric_chart',
- label: 'Chart link',
- property: generatedLink,
- });
- });
-
- it('should return Incident Management event options if located on Metrics Dashboard', () => {
- document.body.dataset.page = 'metrics:show';
-
- expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
- category: 'Incident Management::Embedded metrics',
- action: 'generate_link_to_metrics_chart',
- label: 'Chart link',
- property: generatedLink,
- });
- });
- });
-
- describe('trackDownloadCSVEvent', () => {
- it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
- document.body.dataset.page = 'groups:clusters:show';
-
- expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
- category: 'Cluster Monitoring',
- action: 'download_csv_of_cluster_metric_chart',
- label: 'Chart title',
- property: chartTitle,
- });
- });
-
- it('should return Incident Management event options if located on Metrics Dashboard', () => {
- document.body.dataset.page = 'metriss:show';
-
- expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
- category: 'Incident Management::Embedded metrics',
- action: 'download_csv_of_metrics_dashboard_chart',
- label: 'Chart title',
- property: chartTitle,
- });
- });
- });
-
- describe('graphDataValidatorForValues', () => {
- /*
- * When dealing with a metric using the query format, e.g.
- * query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024'
- * the validator will look for the `value` key instead of `values`
- */
- it('validates data with the query format', () => {
- const validGraphData = monitoringUtils.graphDataValidatorForValues(
- true,
- singleStatGraphData(),
- );
-
- expect(validGraphData).toBe(true);
- });
-
- /*
- * When dealing with a metric using the query?range format, e.g.
- * query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
- * the validator will look for the `values` key instead of `value`
- */
- it('validates data with the query_range format', () => {
- const validGraphData = monitoringUtils.graphDataValidatorForValues(false, graphData);
-
- expect(validGraphData).toBe(true);
- });
- });
-
- describe('graphDataValidatorForAnomalyValues', () => {
- let oneMetric;
- let threeMetrics;
- let fourMetrics;
- beforeEach(() => {
- oneMetric = singleStatGraphData();
- threeMetrics = anomalyGraphData();
-
- const metrics = [...threeMetrics.metrics];
- metrics.push(threeMetrics.metrics[0]);
- fourMetrics = {
- ...anomalyGraphData(),
- metrics,
- };
- });
- /*
- * Anomaly charts can accept results for exactly 3 metrics,
- */
- it('validates passes with the right query format', () => {
- expect(monitoringUtils.graphDataValidatorForAnomalyValues(threeMetrics)).toBe(true);
- });
-
- it('validation fails for wrong format, 1 metric', () => {
- expect(monitoringUtils.graphDataValidatorForAnomalyValues(oneMetric)).toBe(false);
- });
-
- it('validation fails for wrong format, more than 3 metrics', () => {
- expect(monitoringUtils.graphDataValidatorForAnomalyValues(fourMetrics)).toBe(false);
- });
- });
-
- describe('timeRangeFromUrl', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- const { timeRangeFromUrl } = monitoringUtils;
-
- it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
- urlUtils.queryToObject.mockReturnValueOnce(range);
- expect(timeRangeFromUrl()).toEqual(range);
- });
-
- it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
- const { seconds } = rollingRange.duration;
-
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboard/my_dashboard.yml',
- duration_seconds: `${seconds}`,
- });
-
- expect(timeRangeFromUrl()).toEqual(rollingRange);
- });
-
- it('returns null when no time range parameters are given', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboards/custom_dashboard.yml',
- param1: 'value1',
- param2: 'value2',
- });
-
- expect(timeRangeFromUrl()).toBe(null);
- });
- });
-
- describe('templatingVariablesFromUrl', () => {
- const { templatingVariablesFromUrl } = monitoringUtils;
-
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- it('returns an object with only the custom variables', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboards/custom_dashboard.yml',
- y_label: 'memory usage',
- group: 'kubernetes',
- title: 'Kubernetes memory total',
- start: '2020-05-06',
- end: '2020-05-07',
- duration_seconds: '86400',
- direction: 'left',
- anchor: 'top',
- pod: 'POD',
- 'var-pod': 'POD',
- });
-
- expect(templatingVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
- });
-
- it('returns an empty object when no custom variables are present', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboards/custom_dashboard.yml',
- });
-
- expect(templatingVariablesFromUrl()).toStrictEqual({});
- });
- });
-
- describe('removeTimeRangeParams', () => {
- const { removeTimeRangeParams } = monitoringUtils;
-
- it('returns when query contains `start` and `end` parameters are given', () => {
- expect(removeTimeRangeParams(`${mockPath}?start=${range.start}&end=${range.end}`)).toEqual(
- mockPath,
- );
- });
- });
-
- describe('timeRangeToUrl', () => {
- const { timeRangeToUrl } = monitoringUtils;
-
- beforeEach(() => {
- jest.spyOn(urlUtils, 'mergeUrlParams');
- jest.spyOn(urlUtils, 'removeParams');
- });
-
- afterEach(() => {
- urlUtils.mergeUrlParams.mockRestore();
- urlUtils.removeParams.mockRestore();
- });
-
- it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
- const toUrl = `${mockPath}?start=${range.start}&end=${range.end}`;
- const fromUrl = mockPath;
-
- urlUtils.removeParams.mockReturnValueOnce(fromUrl);
- urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
-
- expect(timeRangeToUrl(range)).toEqual(toUrl);
- expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
- });
-
- it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
- const { seconds } = rollingRange.duration;
-
- const toUrl = `${mockPath}?duration_seconds=${seconds}`;
- const fromUrl = mockPath;
-
- urlUtils.removeParams.mockReturnValueOnce(fromUrl);
- urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
-
- expect(timeRangeToUrl(rollingRange)).toEqual(toUrl);
- expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(
- { duration_seconds: `${seconds}` },
- fromUrl,
- );
- });
- });
-
- describe('expandedPanelPayloadFromUrl', () => {
- const { expandedPanelPayloadFromUrl } = monitoringUtils;
- const [panelGroup] = metricsDashboardViewModel.panelGroups;
- const [panel] = panelGroup.panels;
-
- const { group } = panelGroup;
- const { title, y_label: yLabel } = panel;
-
- it('returns payload for a panel when query parameters are given', () => {
- const search = `?group=${group}&title=${title}&y_label=${yLabel}`;
-
- expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toEqual({
- group: panelGroup.group,
- panel,
- });
- });
-
- it('returns null when no parameters are given', () => {
- expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, '')).toBe(null);
- });
-
- it('throws an error when no group is provided', () => {
- const search = `?title=${panel.title}&y_label=${yLabel}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
-
- it('throws an error when no title is provided', () => {
- const search = `?title=${title}&y_label=${yLabel}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
-
- it('throws an error when no y_label group is provided', () => {
- const search = `?group=${group}&title=${title}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
-
- it.each`
- group | title | yLabel | missingField
- ${'NOT_A_GROUP'} | ${title} | ${yLabel} | ${'group'}
- ${group} | ${'NOT_A_TITLE'} | ${yLabel} | ${'title'}
- ${group} | ${title} | ${'NOT_A_Y_LABEL'} | ${'y_label'}
- `('throws an error when $missingField is incorrect', (params) => {
- const search = `?group=${params.group}&title=${params.title}&y_label=${params.yLabel}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
- });
-
- describe('panelToUrl', () => {
- const { panelToUrl } = monitoringUtils;
-
- const dashboard = 'metrics.yml';
- const [panelGroup] = metricsDashboardViewModel.panelGroups;
- const [panel] = panelGroup.panels;
-
- const getUrlParams = (url) => urlUtils.queryToObject(url.split('?')[1]);
-
- it('returns URL for a panel when query parameters are given', () => {
- const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, panel));
-
- expect(params).toEqual(
- expect.objectContaining({
- dashboard,
- group: panelGroup.group,
- title: panel.title,
- y_label: panel.y_label,
- }),
- );
- });
-
- it('returns a dashboard only URL if group is missing', () => {
- const params = getUrlParams(panelToUrl(dashboard, {}, null, panel));
- expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
- });
-
- it('returns a dashboard only URL if panel is missing', () => {
- const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, null));
- expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
- });
-
- it('returns URL for a panel when query paramters are given including custom variables', () => {
- const params = getUrlParams(panelToUrl(dashboard, { pod: 'pod' }, panelGroup.group, null));
- expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml', pod: 'pod' }));
- });
- });
-
- describe('barChartsDataParser', () => {
- const singleMetricExpected = {
- SLA: [
- ['0.9935198135198128', 'api'],
- ['0.9975296513504401', 'git'],
- ['0.9994716394716395', 'registry'],
- ['0.9948251748251747', 'sidekiq'],
- ['0.9535664335664336', 'web'],
- ['0.9335664335664336', 'postgresql_database'],
- ],
- };
-
- const multipleMetricExpected = {
- ...singleMetricExpected,
- SLA_2: Object.values(singleMetricExpected)[0],
- };
-
- const barMockDataWithMultipleMetrics = {
- ...barMockData,
- metrics: [
- barMockData.metrics[0],
- {
- ...barMockData.metrics[0],
- label: 'SLA_2',
- },
- ],
- };
-
- it.each([
- {
- input: { metrics: undefined },
- output: {},
- testCase: 'barChartsDataParser returns {} with undefined',
- },
- {
- input: { metrics: null },
- output: {},
- testCase: 'barChartsDataParser returns {} with null',
- },
- {
- input: { metrics: [] },
- output: {},
- testCase: 'barChartsDataParser returns {} with []',
- },
- {
- input: barMockData,
- output: singleMetricExpected,
- testCase: 'barChartsDataParser returns single series object with single metrics',
- },
- {
- input: barMockDataWithMultipleMetrics,
- output: multipleMetricExpected,
- testCase: 'barChartsDataParser returns multiple series object with multiple metrics',
- },
- ])('$testCase', ({ input, output }) => {
- expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual(
- expect.objectContaining(output),
- );
- });
- });
-
- describe('removePrefixFromLabel', () => {
- it.each`
- input | expected
- ${undefined} | ${''}
- ${null} | ${''}
- ${''} | ${''}
- ${' '} | ${' '}
- ${'pod-1'} | ${'pod-1'}
- ${'pod-var-1'} | ${'pod-var-1'}
- ${'pod-1-var'} | ${'pod-1-var'}
- ${'podvar--1'} | ${'podvar--1'}
- ${'povar-d-1'} | ${'povar-d-1'}
- ${'var-pod-1'} | ${'pod-1'}
- ${'var-var-pod-1'} | ${'var-pod-1'}
- ${'varvar-pod-1'} | ${'varvar-pod-1'}
- ${'var-pod-1-var-'} | ${'pod-1-var-'}
- `('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
- expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
- });
- });
-
- describe('convertVariablesForURL', () => {
- it.each`
- input | expected
- ${[]} | ${{}}
- ${[{ name: 'env', value: 'prod' }]} | ${{ 'var-env': 'prod' }}
- ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${{ 'var-env1': 'prod' }}
- ${[{ name: 'var-env', value: 'prod' }]} | ${{ 'var-var-env': 'prod' }}
- `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
- expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
- });
- });
-
- describe('setCustomVariablesFromUrl', () => {
- beforeEach(() => {
- window.history.pushState = jest.fn();
- jest.spyOn(urlUtils, 'updateHistory');
- });
-
- afterEach(() => {
- urlUtils.updateHistory.mockRestore();
- });
-
- it.each`
- input | urlParams
- ${[]} | ${''}
- ${[{ name: 'env', value: 'prod' }]} | ${'?var-env=prod'}
- ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env1=prod'}
- `(
- 'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input',
- ({ input, urlParams }) => {
- monitoringUtils.setCustomVariablesFromUrl(input);
-
- expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1);
- expect(urlUtils.updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/${urlParams}`,
- title: '',
- });
- },
- );
- });
-});
diff --git a/spec/frontend/monitoring/validators_spec.js b/spec/frontend/monitoring/validators_spec.js
deleted file mode 100644
index 0c3d77a7d98..00000000000
--- a/spec/frontend/monitoring/validators_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { alertsValidator, queriesValidator } from '~/monitoring/validators';
-
-describe('alertsValidator', () => {
- const validAlert = {
- alert_path: 'my/alert.json',
- operator: '<',
- threshold: 5,
- metricId: '8',
- };
- it('requires all alerts to have an alert path', () => {
- const { operator, threshold, metricId } = validAlert;
- const input = {
- [validAlert.alert_path]: {
- operator,
- threshold,
- metricId,
- },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires that the object key matches the alert path', () => {
- const input = {
- undefined: validAlert,
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires all alerts to have a metric id', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, metricId: undefined },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires the metricId to be a string', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, metricId: 8 },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires all alerts to have an operator', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, operator: '' },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires all alerts to have an numeric threshold', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, threshold: '60' },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('correctly identifies a valid alerts object', () => {
- const input = {
- [validAlert.alert_path]: validAlert,
- };
- expect(alertsValidator(input)).toEqual(true);
- });
-});
-describe('queriesValidator', () => {
- const validQuery = {
- metricId: '8',
- alert_path: 'alert',
- label: 'alert-label',
- };
- it('requires all alerts to have a metric id', () => {
- const input = [{ ...validQuery, metricId: undefined }];
- expect(queriesValidator(input)).toEqual(false);
- });
- it('requires the metricId to be a string', () => {
- const input = [{ ...validQuery, metricId: 8 }];
- expect(queriesValidator(input)).toEqual(false);
- });
- it('requires all queries to have a label', () => {
- const input = [{ ...validQuery, label: undefined }];
- expect(queriesValidator(input)).toEqual(false);
- });
- it('correctly identifies a valid queries array', () => {
- const input = [validQuery];
- expect(queriesValidator(input)).toEqual(true);
- });
-});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 6c774a1ecd0..a6d88bdd310 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -20,6 +20,7 @@ import eventHub from '~/notes/event_hub';
import { COMMENT_FORM } from '~/notes/i18n';
import notesModule from '~/notes/stores/modules';
import { sprintf } from '~/locale';
+import { mockTracking } from 'helpers/tracking_helper';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
@@ -31,6 +32,7 @@ Vue.use(Vuex);
describe('issue_comment_form component', () => {
useLocalStorageSpy();
+ let trackingSpy;
let store;
let wrapper;
let axiosMock;
@@ -121,6 +123,15 @@ describe('issue_comment_form component', () => {
provide: {
glFeatures: features,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
}),
);
};
@@ -128,6 +139,7 @@ describe('issue_comment_form component', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
store = createStore();
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
afterEach(() => {
@@ -150,6 +162,21 @@ describe('issue_comment_form component', () => {
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
});
+ it('tracks event', () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+ jest.spyOn(wrapper.vm, 'stopPolling');
+
+ findCloseReopenButton().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Issue_comment',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
+
it('does not report errors in the UI when the save succeeds', async () => {
mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
@@ -294,13 +321,13 @@ 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('Switch to rich text');
+ expect(wrapper.text()).not.toContain('Switch to rich text editing');
});
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
- expect(wrapper.text()).toContain('Switch to rich text');
+ expect(wrapper.text()).toContain('Switch to rich text editing');
});
describe('textarea', () => {
@@ -327,9 +354,8 @@ describe('issue_comment_form component', () => {
jest.spyOn(wrapper.vm, 'stopPolling');
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- // 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({ note: 'hello world' });
+ findMarkdownEditor().vm.$emit('input', 'hello world');
+ await nextTick();
await findCommentButton().trigger('click');
@@ -347,15 +373,7 @@ describe('issue_comment_form component', () => {
const { markdownDocsPath } = notesDataMock;
- expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown');
- });
-
- it('should link to quick actions docs', () => {
- mountComponent({ mountFunction: mount });
-
- const { quickActionsDocsPath } = notesDataMock;
-
- expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions');
+ expect(wrapper.find(`[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
it('should resize textarea after note discarded', async () => {
@@ -459,9 +477,8 @@ describe('issue_comment_form component', () => {
it('should enable comment button if it has note', async () => {
mountComponent();
- // 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({ note: 'Foo' });
+ findMarkdownEditor().vm.$emit('input', 'Foo');
+ await nextTick();
expect(findCommentTypeDropdown().props('disabled')).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 b891c1f553d..053542a421c 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlButton, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
@@ -8,9 +8,9 @@ import { COMMENT_FORM } from '~/notes/i18n';
describe('CommentTypeDropdown component', () => {
let wrapper;
- const findCommentGlDropdown = () => wrapper.findComponent(GlDropdown);
- const findCommentDropdownOption = () => wrapper.findAllComponents(GlDropdownItem).at(0);
- const findDiscussionDropdownOption = () => wrapper.findAllComponents(GlDropdownItem).at(1);
+ const findCommentButton = () => wrapper.findComponent(GlButton);
+ const findCommentListboxOption = () => wrapper.findAllComponents(GlListboxItem).at(0);
+ const findDiscussionListboxOption = () => wrapper.findAllComponents(GlListboxItem).at(1);
const mountComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
@@ -20,6 +20,10 @@ describe('CommentTypeDropdown component', () => {
noteType: constants.COMMENT,
...props,
},
+ stubs: {
+ GlCollapsibleListbox,
+ GlListboxItem,
+ },
}),
);
};
@@ -33,15 +37,15 @@ describe('CommentTypeDropdown component', () => {
({ isInternalNote, buttonText }) => {
mountComponent({ props: { noteType: constants.COMMENT, isInternalNote } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ expect(findCommentButton().text()).toBe(buttonText);
},
);
it('Should set correct dropdown item checked when comment is selected', () => {
mountComponent({ props: { noteType: constants.COMMENT } });
- expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: true });
- expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: false });
+ expect(findCommentListboxOption().props('isSelected')).toBe(true);
+ expect(findDiscussionListboxOption().props('isSelected')).toBe(false);
});
it.each`
@@ -53,32 +57,22 @@ describe('CommentTypeDropdown component', () => {
({ isInternalNote, buttonText }) => {
mountComponent({ props: { noteType: constants.DISCUSSION, isInternalNote } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ expect(findCommentButton().text()).toBe(buttonText);
},
);
it('Should set correct dropdown item option checked when discussion is selected', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: false });
- expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: true });
+ expect(findCommentListboxOption().props('isSelected')).toBe(false);
+ expect(findDiscussionListboxOption().props('isSelected')).toBe(true);
});
it('Should emit `change` event when clicking on an alternate dropdown option', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- const event = {
- type: 'click',
- stopPropagation: jest.fn(),
- preventDefault: jest.fn(),
- };
-
- findCommentDropdownOption().vm.$emit('click', event);
- findDiscussionDropdownOption().vm.$emit('click', event);
-
- // ensure the native events don't trigger anything
- expect(event.stopPropagation).toHaveBeenCalledTimes(2);
- expect(event.preventDefault).toHaveBeenCalledTimes(2);
+ findCommentListboxOption().trigger('click');
+ findDiscussionListboxOption().trigger('click');
expect(wrapper.emitted('change')[0]).toEqual([constants.COMMENT]);
expect(wrapper.emitted('change').length).toEqual(1);
@@ -87,7 +81,7 @@ describe('CommentTypeDropdown component', () => {
it('Should emit `click` event when clicking on the action button', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- findCommentGlDropdown().vm.$emit('click');
+ findCommentButton().vm.$emit('click');
expect(wrapper.emitted('click').length > 0).toBe(true);
});
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 66b86ed3ce0..123d53de3f3 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -12,14 +12,21 @@ describe('diff_discussion_header component', () => {
let store;
let wrapper;
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMount(diffDiscussionHeader, {
+ store,
+ propsData: {
+ discussion: discussionMock,
+ ...propsData,
+ },
+ });
+ };
+
beforeEach(() => {
window.mrTabs = {};
store = createStore();
- wrapper = shallowMount(diffDiscussionHeader, {
- store,
- propsData: { discussion: discussionMock },
- });
+ createComponent({ propsData: { discussion: discussionMock } });
});
describe('Avatar', () => {
@@ -27,19 +34,23 @@ describe('diff_discussion_header component', () => {
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findAvatar = () => wrapper.findComponent(GlAvatar);
- it('should render user avatar and user avatar link', () => {
+ it('should render user avatar and user avatar link with popover support', () => {
expect(findAvatar().exists()).toBe(true);
- expect(findAvatarLink().exists()).toBe(true);
+
+ const avatarLink = findAvatarLink();
+ expect(avatarLink.exists()).toBe(true);
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: firstNoteAuthor.path,
+ 'data-user-id': `${firstNoteAuthor.id}`,
+ 'data-username': `${firstNoteAuthor.username}`,
+ });
});
it('renders avatar of the first note author', () => {
- const props = findAvatar().props();
-
- expect(props).toMatchObject({
- src: firstNoteAuthor.avatar_url,
- alt: firstNoteAuthor.name,
- size: 32,
- });
+ expect(findAvatar().props('src')).toBe(firstNoteAuthor.avatar_url);
+ expect(findAvatar().props('alt')).toBe(firstNoteAuthor.name);
+ expect(findAvatar().props('size')).toBe(32);
});
});
@@ -53,14 +64,16 @@ describe('diff_discussion_header component', () => {
projectPath: 'something',
};
- wrapper.setProps({
- discussion: {
- ...discussionMock,
- for_commit: true,
- commit_id: commitId,
- diff_discussion: true,
- diff_file: {
- ...mockDiffFile,
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ for_commit: true,
+ commit_id: commitId,
+ diff_discussion: true,
+ diff_file: {
+ ...mockDiffFile,
+ },
},
},
});
@@ -71,9 +84,15 @@ describe('diff_discussion_header component', () => {
describe('for diff threads without a commit id', () => {
it('should show started a thread on the diff text', async () => {
- Object.assign(wrapper.vm.discussion, {
- for_commit: false,
- commit_id: null,
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: null,
+ },
+ },
});
await nextTick();
@@ -81,10 +100,16 @@ describe('diff_discussion_header component', () => {
});
it('should show thread on older version text', async () => {
- Object.assign(wrapper.vm.discussion, {
- for_commit: false,
- commit_id: null,
- active: false,
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: null,
+ active: false,
+ },
+ },
});
await nextTick();
@@ -102,7 +127,16 @@ describe('diff_discussion_header component', () => {
describe('for diff thread with a commit id', () => {
it('should display started thread on commit header', async () => {
- wrapper.vm.discussion.for_commit = false;
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: commitId,
+ },
+ },
+ });
await nextTick();
expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
@@ -111,8 +145,17 @@ describe('diff_discussion_header component', () => {
});
it('should display outdated change on commit header', async () => {
- wrapper.vm.discussion.for_commit = false;
- wrapper.vm.discussion.active = false;
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: commitId,
+ active: false,
+ },
+ },
+ });
await nextTick();
expect(wrapper.text()).toContain(
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index ac677841ee1..e52dd87f784 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -1,11 +1,11 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import notesModule from '~/notes/stores/modules';
import * as types from '~/notes/stores/mutation_types';
-import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
+import { discussionMock, noteableDataMock, notesDataMock, userDataMock } from '../mock_data';
describe('DiscussionCounter component', () => {
let store;
@@ -101,9 +101,24 @@ describe('DiscussionCounter component', () => {
`('renders correctly if $title', async ({ resolved, groupLength }) => {
updateStore({ resolvable: true, resolved });
wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- await wrapper.find('.dropdown-toggle').trigger('click');
+ await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(groupLength);
+ expect(wrapper.findAllComponents(GlDisclosureDropdownItem)).toHaveLength(groupLength);
+ });
+
+ describe('resolve all with new issue link', () => {
+ it('has correct href prop', async () => {
+ updateStore({ resolvable: true });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+
+ const resolveDiscussionsPath =
+ store.getters.getNoteableData.create_issue_to_resolve_discussions_path;
+
+ await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
+ const resolveAllLink = wrapper.find('[data-testid="resolve-all-with-issue-link"]');
+
+ expect(resolveAllLink.attributes('href')).toBe(resolveDiscussionsPath);
+ });
});
});
@@ -114,7 +129,7 @@ describe('DiscussionCounter component', () => {
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- await wrapper.find('.dropdown-toggle').trigger('click');
+ await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]');
};
diff --git a/spec/frontend/notes/components/mr_discussion_filter_spec.js b/spec/frontend/notes/components/mr_discussion_filter_spec.js
index beb25c30af6..2bb47fd3c9e 100644
--- a/spec/frontend/notes/components/mr_discussion_filter_spec.js
+++ b/spec/frontend/notes/components/mr_discussion_filter_spec.js
@@ -67,7 +67,7 @@ describe('Merge request discussion filter component', () => {
it('lists current filters', () => {
createComponent();
- expect(wrapper.findAllComponents(GlListboxItem).length).toBe(MR_FILTER_OPTIONS.length);
+ expect(wrapper.findAllComponents(GlListboxItem)).toHaveLength(MR_FILTER_OPTIONS.length);
});
it('updates store when selecting filter', async () => {
@@ -107,4 +107,30 @@ describe('Merge request discussion filter component', () => {
expect(wrapper.findComponent(GlButton).text()).toBe(expectedText);
});
+
+ it('when clicking de-select it de-selects all options', async () => {
+ createComponent();
+
+ wrapper.find('[data-testid="listbox-reset-button"]').vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(0);
+ });
+
+ it('when clicking select all it selects all options', async () => {
+ createComponent();
+
+ wrapper.find('[data-testid="listbox-item-approval"]').vm.$emit('select', false);
+
+ await nextTick();
+
+ expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(9);
+
+ wrapper.find('[data-testid="listbox-select-all-button"]').vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(10);
+ });
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index b5b33607282..645aef21e38 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -7,6 +7,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete';
import eventHub from '~/environments/event_hub';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
@@ -15,6 +16,7 @@ describe('issue_note_form component', () => {
let store;
let wrapper;
let props;
+ let trackingSpy;
const createComponentWrapper = (propsData = {}, provide = {}) => {
wrapper = mountExtended(NoteForm, {
@@ -26,6 +28,15 @@ describe('issue_note_form component', () => {
provide: {
glFeatures: provide,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
@@ -43,6 +54,7 @@ describe('issue_note_form component', () => {
noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.',
noteId: '545',
};
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
describe('noteHash', () => {
@@ -66,13 +78,13 @@ describe('issue_note_form component', () => {
it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
createComponentWrapper({}, { contentEditorOnIssues: false });
- expect(wrapper.text()).not.toContain('Switch to rich text');
+ expect(wrapper.text()).not.toContain('Switch to rich text editing');
});
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
createComponentWrapper({}, { contentEditorOnIssues: true });
- expect(wrapper.text()).toContain('Switch to rich text');
+ expect(wrapper.text()).toContain('Switch to rich text editing');
});
describe('conflicts editing', () => {
@@ -213,6 +225,21 @@ describe('issue_note_form component', () => {
expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
+
+ it('tracks event when save button is clicked', () => {
+ createComponentWrapper();
+
+ const textarea = wrapper.find('textarea');
+ textarea.setValue('Foo');
+ const saveButton = wrapper.find('.js-vue-issue-save');
+ saveButton.vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Issue_note',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
});
});
@@ -271,7 +298,9 @@ describe('issue_note_form component', () => {
await nextTick();
- expect(wrapper.emitted('handleFormUpdateAddToReview')).toEqual([['Foo', false]]);
+ expect(wrapper.emitted('handleFormUpdateAddToReview')).toStrictEqual([
+ ['Foo', false, wrapper.vm.$refs.editNoteForm, expect.any(Function)],
+ ]);
});
});
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index d50fb130a69..059972df56b 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { clone } from 'lodash';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -218,6 +218,18 @@ describe('issue_note', () => {
});
});
+ it('should render user avatar link with popover support', () => {
+ const { author } = note;
+ const avatarLink = wrapper.findComponent(GlAvatarLink);
+
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: author.path,
+ 'data-user-id': `${author.id}`,
+ 'data-username': `${author.username}`,
+ });
+ });
+
it('should render user avatar', () => {
const { author } = note;
const avatar = wrapper.findComponent(GlAvatar);
@@ -373,10 +385,24 @@ describe('issue_note', () => {
afterEach(() => updateNote.mockReset());
- it('responds to handleFormUpdate', () => {
+ it('emits handleUpdateNote', () => {
+ const updatedNote = { ...note, note_html: `<p dir="auto">${params.noteText}</p>\n` };
+
findNoteBody().vm.$emit('handleFormUpdate', params);
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
+
+ expect(wrapper.emitted('handleUpdateNote')[0]).toEqual([
+ {
+ note: updatedNote,
+ noteText: params.noteText,
+ resolveDiscussion: params.resolveDiscussion,
+ position: {},
+ flashContainer: wrapper.vm.$el,
+ callback: expect.any(Function),
+ errorCallback: expect.any(Function),
+ },
+ ]);
});
it('updates note content', async () => {
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 0f70b264326..caf47febedd 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -122,7 +122,7 @@ describe('note_app', () => {
);
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/410409
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/410409
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should render form comment button as disabled', () => {
expect(findCommentButton().props('disabled')).toEqual(true);
@@ -250,15 +250,7 @@ describe('note_app', () => {
it('should render markdown docs url', () => {
const { markdownDocsPath } = mockData.notesDataMock;
- expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text().trim()).toEqual('Markdown');
- });
-
- it('should render quick action docs url', () => {
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
- 'quick actions',
- );
+ expect(wrapper.find(`a[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
});
@@ -274,19 +266,7 @@ describe('note_app', () => {
const { markdownDocsPath } = mockData.notesDataMock;
await nextTick();
- expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual(
- 'Markdown',
- );
- });
-
- it('should render quick actions docs url', async () => {
- wrapper.find('.js-note-edit').trigger('click');
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- await nextTick();
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
- 'quick actions',
- );
+ expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
});
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 355ecb78187..0e0af3f0480 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -32,8 +32,7 @@ function wrappedDiscussionNote(note) {
return `<table><tbody>${note}</tbody></table>`;
}
-// the following test is unreliable and failing in main 2-3 times a day
-// see https://gitlab.com/gitlab-org/gitlab/issues/206906#note_290602581
+// quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/208441
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index b6a2b318ec3..bef8ed8e659 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -74,7 +74,6 @@ describe('Discussion navigation mixin', () => {
});
afterEach(() => {
- jest.clearAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index d5b7ad73177..94549c4a73b 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -60,7 +60,7 @@ export const noteableDataMock = {
updated_at: '2017-08-04T09:53:01.226Z',
updated_by_id: 1,
web_url: '/gitlab-org/gitlab-foss/issues/26',
- noteableType: 'issue',
+ noteableType: 'Issue',
blocked_by_issues: [],
};
diff --git a/spec/frontend/notes/utils_spec.js b/spec/frontend/notes/utils_spec.js
index 0882e0a5759..3607c3c546c 100644
--- a/spec/frontend/notes/utils_spec.js
+++ b/spec/frontend/notes/utils_spec.js
@@ -1,12 +1,12 @@
import { sprintf } from '~/locale';
-import { getErrorMessages } from '~/notes/utils';
+import { createNoteErrorMessages, updateNoteErrorMessage } from '~/notes/utils';
import { HTTP_STATUS_UNPROCESSABLE_ENTITY, HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
-import { COMMENT_FORM } from '~/notes/i18n';
+import { COMMENT_FORM, UPDATE_COMMENT_FORM } from '~/notes/i18n';
-describe('getErrorMessages', () => {
+describe('createNoteErrorMessages', () => {
describe('when http status is not HTTP_STATUS_UNPROCESSABLE_ENTITY', () => {
it('returns generic error', () => {
- const errorMessages = getErrorMessages(
+ const errorMessages = createNoteErrorMessages(
{ errors: ['unknown error'] },
HTTP_STATUS_BAD_REQUEST,
);
@@ -17,7 +17,7 @@ describe('getErrorMessages', () => {
describe('when http status is HTTP_STATUS_UNPROCESSABLE_ENTITY', () => {
it('returns all errors', () => {
- const errorMessages = getErrorMessages(
+ const errorMessages = createNoteErrorMessages(
{ errors: 'error 1 and error 2' },
HTTP_STATUS_UNPROCESSABLE_ENTITY,
);
@@ -29,7 +29,7 @@ describe('getErrorMessages', () => {
describe('when response contains commands_only errors', () => {
it('only returns commands_only errors', () => {
- const errorMessages = getErrorMessages(
+ const errorMessages = createNoteErrorMessages(
{
errors: {
commands_only: ['commands_only error 1', 'commands_only error 2'],
@@ -44,3 +44,22 @@ describe('getErrorMessages', () => {
});
});
});
+
+describe('updateNoteErrorMessage', () => {
+ describe('with server error', () => {
+ it('returns error message with server error', () => {
+ const error = 'error 1 and error 2';
+ const errorMessage = updateNoteErrorMessage({ response: { data: { errors: error } } });
+
+ expect(errorMessage).toEqual(sprintf(UPDATE_COMMENT_FORM.error, { reason: error }));
+ });
+ });
+
+ describe('without server error', () => {
+ it('returns generic error message', () => {
+ const errorMessage = updateNoteErrorMessage(null);
+
+ expect(errorMessage).toEqual(UPDATE_COMMENT_FORM.defaultError);
+ });
+ });
+});
diff --git a/spec/frontend/notifications/components/notification_email_listbox_input_spec.js b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
index c490c737cf1..a3a847b9523 100644
--- a/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
+++ b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
@@ -13,6 +13,7 @@ describe('NotificationEmailListboxInput', () => {
const emptyValueText = 'emptyValueText';
const value = 'value';
const disabled = false;
+ const placement = 'right';
// Finders
const findListboxInput = () => wrapper.findComponent(ListboxInput);
@@ -26,6 +27,7 @@ describe('NotificationEmailListboxInput', () => {
emptyValueText,
value,
disabled,
+ placement,
},
attachTo,
});
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
new file mode 100644
index 00000000000..239d7adf986
--- /dev/null
+++ b/spec/frontend/observability/client_spec.js
@@ -0,0 +1,66 @@
+import MockAdapter from 'axios-mock-adapter';
+import { buildClient } from '~/observability/client';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/lib/utils/axios_utils');
+
+describe('buildClient', () => {
+ let client;
+ let axiosMock;
+
+ const tracingUrl = 'https://example.com/tracing';
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ jest.spyOn(axios, 'get');
+
+ client = buildClient({
+ tracingUrl,
+ provisioningUrl: 'https://example.com/provisioning',
+ });
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ describe('fetchTraces', () => {
+ it('should fetch traces from the tracing URL', async () => {
+ const mockTraces = [
+ { id: 1, spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
+ { id: 2, spans: [{ duration_nano: 2000 }] },
+ ];
+
+ axiosMock.onGet(tracingUrl).reply(200, {
+ traces: mockTraces,
+ });
+
+ const result = await client.fetchTraces();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual([
+ { id: 1, spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }], duration: 3 },
+ { id: 2, spans: [{ duration_nano: 2000 }], duration: 2 },
+ ]);
+ });
+
+ it('rejects if traces are missing', () => {
+ axiosMock.onGet(tracingUrl).reply(200, {});
+
+ return expect(client.fetchTraces()).rejects.toThrow(
+ 'traces are missing/invalid in the response',
+ );
+ });
+
+ it('rejects if traces are invalid', () => {
+ axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
+
+ return expect(client.fetchTraces()).rejects.toThrow(
+ 'traces are missing/invalid in the response',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index 4a9be71b880..392992a5962 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -1,4 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import ObservabilityApp from '~/observability/components/observability_app.vue';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
import {
@@ -21,7 +22,7 @@ describe('ObservabilityApp', () => {
query: { otherQuery: 100 },
};
- const mockHandleSkeleton = jest.fn();
+ const mockSkeletonOnContentLoaded = jest.fn();
const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
@@ -36,7 +37,9 @@ describe('ObservabilityApp', () => {
...props,
},
stubs: {
- 'observability-skeleton': ObservabilitySkeleton,
+ ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
+ methods: { onContentLoaded: mockSkeletonOnContentLoaded },
+ }),
},
mocks: {
$route,
@@ -155,14 +158,14 @@ describe('ObservabilityApp', () => {
describe('on GOUI_LOADED', () => {
beforeEach(() => {
mountComponent();
- wrapper.vm.$refs.observabilitySkeleton.onContentLoaded = mockHandleSkeleton;
});
+
it('should call onContentLoaded method', () => {
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://observe.gitlab.com',
});
- expect(mockHandleSkeleton).toHaveBeenCalled();
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalled();
});
it('should not call onContentLoaded method if origin is different', () => {
@@ -170,7 +173,7 @@ describe('ObservabilityApp', () => {
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://example.com',
});
- expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ expect(mockSkeletonOnContentLoaded).not.toHaveBeenCalled();
});
it('should not call onContentLoaded method if event type is different', () => {
@@ -178,7 +181,7 @@ describe('ObservabilityApp', () => {
data: { type: 'UNKNOWN_EVENT' },
origin: 'https://observe.gitlab.com',
});
- expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ expect(mockSkeletonOnContentLoaded).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/observability/observability_container_spec.js b/spec/frontend/observability/observability_container_spec.js
new file mode 100644
index 00000000000..1152df072d4
--- /dev/null
+++ b/spec/frontend/observability/observability_container_spec.js
@@ -0,0 +1,134 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import { buildClient } from '~/observability/client';
+
+jest.mock('~/observability/client');
+
+describe('ObservabilityContainer', () => {
+ let wrapper;
+
+ const mockSkeletonOnContentLoaded = jest.fn();
+ const mockSkeletonOnError = jest.fn();
+
+ const OAUTH_URL = 'https://example.com/oauth';
+ const TRACING_URL = 'https://example.com/tracing';
+ const PROVISIONING_URL = 'https://example.com/provisioning';
+
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+
+ buildClient.mockReturnValue({});
+
+ wrapper = shallowMountExtended(ObservabilityContainer, {
+ propsData: {
+ oauthUrl: OAUTH_URL,
+ tracingUrl: TRACING_URL,
+ provisioningUrl: PROVISIONING_URL,
+ },
+ stubs: {
+ ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
+ methods: { onContentLoaded: mockSkeletonOnContentLoaded, onError: mockSkeletonOnError },
+ }),
+ },
+ slots: {
+ default: {
+ render(h) {
+ h(`<div>mockedComponent</div>`);
+ },
+ name: 'MockComponent',
+ props: {
+ observabilityClient: {
+ type: Object,
+ required: true,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ const dispatchMessageEvent = (status, origin) =>
+ window.dispatchEvent(
+ new MessageEvent('message', {
+ data: {
+ type: 'AUTH_COMPLETION',
+ status,
+ },
+ origin: origin ?? new URL(OAUTH_URL).origin,
+ }),
+ );
+
+ const findIframe = () => wrapper.findByTestId('observability-oauth-iframe');
+ const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+
+ it('should render the oauth iframe', () => {
+ const iframe = findIframe();
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('hidden')).toBe('hidden');
+ expect(iframe.attributes('src')).toBe(OAUTH_URL);
+ expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
+ });
+
+ it('should render the ObservabilitySkeleton', () => {
+ const skeleton = wrapper.findComponent(ObservabilitySkeleton);
+ expect(skeleton.exists()).toBe(true);
+ });
+
+ it('should not render the default slot', () => {
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ it('renders the slot content and removes the iframe on oauth success message', async () => {
+ dispatchMessageEvent('success');
+
+ await nextTick();
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+
+ const slotComponent = findSlotComponent();
+ expect(slotComponent.exists()).toBe(true);
+ expect(buildClient).toHaveBeenCalledWith({
+ provisioningUrl: PROVISIONING_URL,
+ tracingUrl: TRACING_URL,
+ });
+ expect(findIframe().exists()).toBe(false);
+ });
+
+ it('does not render the slot content and removes the iframe on oauth error message', async () => {
+ dispatchMessageEvent('error');
+
+ await nextTick();
+
+ expect(mockSkeletonOnError).toHaveBeenCalledTimes(1);
+
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(findIframe().exists()).toBe(false);
+ expect(buildClient).not.toHaveBeenCalled();
+ });
+
+ it('handles oauth message only once', () => {
+ dispatchMessageEvent('success');
+ dispatchMessageEvent('success');
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ });
+
+ it('only handles messages from the oauth url', () => {
+ dispatchMessageEvent('success', 'www.fake-url.com');
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(findIframe().exists()).toBe(true);
+ });
+
+ it('does not handle messages if the component has been destroyed', () => {
+ wrapper.destroy();
+
+ dispatchMessageEvent('success');
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
index 65dbb003743..979070cfb12 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
+import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Skeleton from '~/observability/components/skeleton/index.vue';
@@ -17,9 +17,9 @@ import {
describe('Skeleton component', () => {
let wrapper;
- const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+ const SKELETON_VARIANTS = [...Object.values(SKELETON_VARIANTS_BY_ROUTE), 'spinner'];
- const findContentWrapper = () => wrapper.findByTestId('observability-wrapper');
+ const findContentWrapper = () => wrapper.findByTestId('content-wrapper');
const findExploreSkeleton = () => wrapper.findComponent(ExploreSkeleton);
@@ -42,8 +42,8 @@ describe('Skeleton component', () => {
mountComponent({ variant: 'explore' });
});
- describe('loading timers', () => {
- it('show Skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
+ describe('showing content', () => {
+ it('shows the skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
expect(findExploreSkeleton().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(false);
@@ -55,7 +55,7 @@ describe('Skeleton component', () => {
expect(findContentWrapper().isVisible()).toBe(false);
});
- it('does not show the skeleton if content has loaded within CONTENT_WAIT_MS', async () => {
+ it('does not show the skeleton if content loads within CONTENT_WAIT_MS', async () => {
expect(findExploreSkeleton().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(false);
@@ -73,9 +73,25 @@ describe('Skeleton component', () => {
expect(findContentWrapper().isVisible()).toBe(true);
expect(findExploreSkeleton().exists()).toBe(false);
});
+
+ it('hides the skeleton after content loads', async () => {
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+
+ await nextTick();
+
+ expect(findExploreSkeleton().exists()).toBe(true);
+ expect(findContentWrapper().isVisible()).toBe(false);
+
+ wrapper.vm.onContentLoaded();
+
+ await nextTick();
+
+ expect(findContentWrapper().isVisible()).toBe(true);
+ expect(findExploreSkeleton().exists()).toBe(false);
+ });
});
- describe('error timeout', () => {
+ describe('error handling', () => {
it('shows the error dialog if content has not loaded within TIMEOUT_MS', async () => {
expect(findAlert().exists()).toBe(false);
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
@@ -86,6 +102,17 @@ describe('Skeleton component', () => {
expect(findContentWrapper().isVisible()).toBe(false);
});
+ it('shows the error dialog if content fails to load', async () => {
+ expect(findAlert().exists()).toBe(false);
+
+ wrapper.vm.onError();
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findContentWrapper().isVisible()).toBe(false);
+ });
+
it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
wrapper.vm.onContentLoaded();
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
@@ -105,6 +132,7 @@ describe('Skeleton component', () => {
${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
${'embed'} | ${'variant is embed'} | ${SKELETON_VARIANT_EMBED}
+ ${'spinner'} | ${'variant is spinner'} | ${'spinner'}
${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
`('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
mountComponent({ variant });
@@ -120,6 +148,8 @@ describe('Skeleton component', () => {
expect(findEmbedSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANT_EMBED);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(variant === 'spinner');
});
});
diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
new file mode 100644
index 00000000000..24e1a26336c
--- /dev/null
+++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
@@ -0,0 +1,99 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import App from '~/organizations/groups_and_projects/components/app.vue';
+import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { organizationProjects } from './mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+describe('GroupsAndProjectsApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('renders loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API call is successful', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ProjectsList` component and passes correct props', async () => {
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(ProjectsList).props()).toEqual({
+ projects: organizationProjects.projects.nodes.map(
+ ({ id, nameWithNamespace, accessLevel, ...project }) => ({
+ ...project,
+ id: getIdFromGraphQLId(id),
+ name: nameWithNamespace,
+ permissions: {
+ projectAccess: {
+ accessLevel: accessLevel.integerValue,
+ },
+ },
+ }),
+ ),
+ showProjectIcon: true,
+ });
+ });
+ });
+
+ describe('when API call is not successful', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('displays error alert', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: App.i18n.errorMessage,
+ error,
+ captureError: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/groups_and_projects/components/mock_data.js b/spec/frontend/organizations/groups_and_projects/components/mock_data.js
new file mode 100644
index 00000000000..c3276450745
--- /dev/null
+++ b/spec/frontend/organizations/groups_and_projects/components/mock_data.js
@@ -0,0 +1,98 @@
+export const organizationProjects = {
+ id: 'gid://gitlab/Organization/1',
+ __typename: 'Organization',
+ projects: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Project/8',
+ nameWithNamespace: 'Twitter / Typeahead.Js',
+ webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
+ topics: ['JavaScript', 'Vue.js'],
+ forksCount: 4,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'public',
+ openIssuesCount: 48,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/7',
+ nameWithNamespace: 'Flightjs / Flight',
+ webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 37,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/6',
+ nameWithNamespace: 'Jashkenas / Underscore',
+ webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/5',
+ nameWithNamespace: 'Commit451 / Lab Coat',
+ webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 49,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/1',
+ nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
+ webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ ],
+ },
+};
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 f74dfcb029d..d4b69d3e8e8 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
@@ -1,5 +1,11 @@
-import { GlFormCheckbox, GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import {
+ GlFormCheckbox,
+ GlSprintf,
+ GlIcon,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -37,16 +43,17 @@ describe('tags list row', () => {
const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]');
const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]');
const findWarningIcon = () => wrapper.findComponent(GlIcon);
- const findAdditionalActionsMenu = () => wrapper.findComponent(GlDropdown);
- const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
+ const findAdditionalActionsMenu = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDeleteButton = () => wrapper.findComponent(GlDisclosureDropdownItem);
- const mountComponent = (propsData = defaultProps) => {
- wrapper = shallowMount(component, {
+ const mountComponent = (propsData = defaultProps, mountFn = shallowMount) => {
+ wrapper = mountFn(component, {
stubs: {
GlSprintf,
ListItem,
DetailsRow,
- GlDropdown,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
},
propsData,
directives: {
@@ -274,10 +281,10 @@ describe('tags list row', () => {
expect(findAdditionalActionsMenu().props()).toMatchObject({
icon: 'ellipsis_v',
- text: 'More actions',
+ toggleText: 'More actions',
textSrOnly: true,
category: 'tertiary',
- right: true,
+ placement: 'right',
disabled: false,
});
});
@@ -308,16 +315,19 @@ describe('tags list row', () => {
mountComponent();
expect(findDeleteButton().exists()).toBe(true);
- expect(findDeleteButton().attributes()).toMatchObject({
- variant: 'danger',
+ expect(findDeleteButton().props('item').extraAttrs).toMatchObject({
+ class: 'gl-text-red-500!',
+ 'data-testid': 'single-delete-button',
+ 'data-qa-selector': 'tag_delete_button',
});
+
expect(findDeleteButton().text()).toBe(REMOVE_TAG_BUTTON_TITLE);
});
it('delete event emits delete', () => {
- mountComponent();
+ mountComponent(undefined, mount);
- findDeleteButton().vm.$emit('click');
+ wrapper.find('[data-testid="single-delete-button"]').trigger('click');
expect(wrapper.emitted('delete')).toEqual([[]]);
});
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 1928dbf72b6..f590cff0312 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -6,7 +6,7 @@ import {
GlFormGroup,
GlModal,
GlSprintf,
- GlEmptyState,
+ GlSkeletonLoader,
} from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -78,7 +78,7 @@ describe('DependencyProxyApp', () => {
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findClearCacheDropdownList = () => wrapper.findComponent(GlDropdown);
const findClearCacheModal = () => wrapper.findComponent(GlModal);
const findClearCacheAlert = () => wrapper.findComponent(GlAlert);
@@ -102,9 +102,16 @@ describe('DependencyProxyApp', () => {
describe('when the dependency proxy is available', () => {
describe('when is loading', () => {
- it('does not render a form group with label', () => {
+ beforeEach(() => {
createComponent();
+ });
+
+ it('renders loading component & sets loading prop', () => {
+ expect(findLoader().exists()).toBe(true);
+ expect(findManifestList().props('loading')).toBe(true);
+ });
+ it('does not render a form group with label', () => {
expect(findFormGroup().exists()).toBe(false);
});
});
@@ -120,11 +127,15 @@ describe('DependencyProxyApp', () => {
expect(findFormGroup().attributes('label')).toBe(
DependencyProxyApp.i18n.proxyImagePrefix,
);
+ expect(findFormGroup().attributes('labelfor')).toBe('proxy-url');
});
it('renders a form input group', () => {
expect(findFormInputGroup().exists()).toBe(true);
+ expect(findFormInputGroup().attributes('id')).toBe('proxy-url');
expect(findFormInputGroup().props('value')).toBe(proxyData().dependencyProxyImagePrefix);
+ expect(findFormInputGroup().attributes('readonly')).toBeDefined();
+ expect(findFormInputGroup().props('selectOnClick')).toBe(true);
});
it('form input group has a clipboard button', () => {
@@ -175,23 +186,12 @@ describe('DependencyProxyApp', () => {
return waitForPromises();
});
- it('shows the empty state message', () => {
- expect(findEmptyState().props()).toMatchObject({
- svgPath: provideDefaults.noManifestsIllustration,
- title: DependencyProxyApp.i18n.noManifestTitle,
- });
- });
-
- it('hides the list', () => {
- expect(findManifestList().exists()).toBe(false);
+ it('renders the list', () => {
+ expect(findManifestList().exists()).toBe(true);
});
});
describe('when there are manifests', () => {
- it('hides the empty state message', () => {
- expect(findEmptyState().exists()).toBe(false);
- });
-
it('shows list', () => {
expect(findManifestList().props()).toMatchObject({
dependencyProxyImagePrefix: proxyData().dependencyProxyImagePrefix,
@@ -200,26 +200,58 @@ describe('DependencyProxyApp', () => {
});
});
- it('prev-page event on list fetches the previous page', async () => {
- findManifestList().vm.$emit('prev-page');
- await waitForPromises();
+ describe('prev-page event on list', () => {
+ beforeEach(() => {
+ findManifestList().vm.$emit('prev-page');
+ });
+
+ describe('while loading', () => {
+ it('does not render loading component & sets loading prop', () => {
+ expect(findLoader().exists()).toBe(false);
+ expect(findManifestList().props('loading')).toBe(true);
+ });
- expect(resolver).toHaveBeenCalledWith({
- before: pagination().startCursor,
- first: null,
- fullPath: provideDefaults.groupPath,
- last: GRAPHQL_PAGE_SIZE,
+ it('renders form group with label', () => {
+ expect(findFormGroup().exists()).toBe(true);
+ });
+ });
+
+ it('list fetches the previous page', async () => {
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ before: pagination().startCursor,
+ first: null,
+ fullPath: provideDefaults.groupPath,
+ last: GRAPHQL_PAGE_SIZE,
+ });
});
});
- it('next-page event on list fetches the next page', async () => {
- findManifestList().vm.$emit('next-page');
- await waitForPromises();
+ describe('next-page event on list', () => {
+ beforeEach(() => {
+ findManifestList().vm.$emit('next-page');
+ });
- expect(resolver).toHaveBeenCalledWith({
- after: pagination().endCursor,
- first: GRAPHQL_PAGE_SIZE,
- fullPath: provideDefaults.groupPath,
+ describe('while loading', () => {
+ it('does not render loading component & sets loading prop', () => {
+ expect(findLoader().exists()).toBe(false);
+ expect(findManifestList().props('loading')).toBe(true);
+ });
+
+ it('renders form group with label', () => {
+ expect(findFormGroup().exists()).toBe(true);
+ });
+ });
+
+ it('fetches the next page', async () => {
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ after: pagination().endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ fullPath: provideDefaults.groupPath,
+ });
});
});
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 4149f728cd8..8f445843aa8 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
@@ -1,6 +1,7 @@
import { GlKeysetPagination, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+import ManifestsEmptyState from '~/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue';
import Component from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
proxyData,
@@ -24,6 +25,7 @@ describe('Manifests List', () => {
});
};
+ const findEmptyState = () => wrapper.findComponent(ManifestsEmptyState);
const findRows = () => wrapper.findAllComponents(ManifestRow);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findMainArea = () => wrapper.findByTestId('main-area');
@@ -38,7 +40,13 @@ describe('Manifests List', () => {
it('shows a row for every manifest', () => {
createComponent();
- expect(findRows().length).toBe(defaultProps.manifests.length);
+ expect(findRows()).toHaveLength(defaultProps.manifests.length);
+ });
+
+ it('does not show the empty state component', () => {
+ createComponent();
+
+ expect(findEmptyState().exists()).toBe(false);
});
it('binds a manifest to each row', () => {
@@ -68,6 +76,20 @@ describe('Manifests List', () => {
});
});
+ describe('when there are no manifests', () => {
+ beforeEach(() => {
+ createComponent({ ...defaultProps, manifests: [], pagination: {} });
+ });
+
+ it('shows the empty state component', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('hides the list', () => {
+ expect(findRows()).toHaveLength(0);
+ });
+ });
+
describe('pagination', () => {
it('is hidden when there is no next or prev pages', () => {
createComponent({ ...defaultProps, pagination: {} });
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js
new file mode 100644
index 00000000000..00c1469994b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js
@@ -0,0 +1,81 @@
+import { GlEmptyState, GlFormGroup, GlFormInputGroup, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ManifestsEmptyState from '~/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+describe('manifests empty state', () => {
+ let wrapper;
+
+ const provideDefaults = {
+ noManifestsIllustration: 'noManifestsIllustration',
+ };
+
+ const createComponent = ({ stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(ManifestsEmptyState, {
+ provide: provideDefaults,
+ stubs: {
+ GlEmptyState,
+ GlFormInputGroup,
+ ...stubs,
+ },
+ });
+ };
+
+ const findDocsLink = () => wrapper.findComponent(GlLink);
+ const findEmptyTextDescription = () => wrapper.findAllComponents(GlSprintf).at(0);
+ const findDocumentationTextDescription = () => wrapper.findAllComponents(GlSprintf).at(1);
+ const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the empty state message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: provideDefaults.noManifestsIllustration,
+ title: ManifestsEmptyState.i18n.noManifestTitle,
+ });
+ });
+
+ it('renders correct description', () => {
+ expect(findEmptyTextDescription().attributes('message')).toBe(
+ ManifestsEmptyState.i18n.emptyText,
+ );
+ expect(findDocumentationTextDescription().attributes('message')).toBe(
+ ManifestsEmptyState.i18n.documentationText,
+ );
+ });
+
+ it('renders a form group with a label', () => {
+ expect(findFormGroup().attributes('label')).toBe(ManifestsEmptyState.i18n.codeExampleLabel);
+ expect(findFormGroup().attributes('label-sr-only')).toBeDefined();
+ expect(findFormGroup().attributes('label-for')).toBe('code-example');
+ });
+
+ it('renders a form input group', () => {
+ expect(findFormInputGroup().exists()).toBe(true);
+ expect(findFormInputGroup().attributes('id')).toBe('code-example');
+ expect(findFormInputGroup().props('value')).toBe(ManifestsEmptyState.codeExample);
+ expect(findFormInputGroup().attributes('readonly')).toBeDefined();
+ expect(findFormInputGroup().props('selectOnClick')).toBe(true);
+ });
+
+ it('form input group has a clipboard button', () => {
+ expect(findClipBoardButton().exists()).toBe(true);
+ expect(findClipBoardButton().props()).toMatchObject({
+ text: ManifestsEmptyState.codeExample,
+ title: ManifestsEmptyState.i18n.copyExample,
+ });
+ });
+
+ it('shows link to docs', () => {
+ createComponent({ stubs: { GlSprintf } });
+
+ expect(findDocsLink().attributes('href')).toBe(
+ ManifestsEmptyState.links.DEPENDENCY_PROXY_HELP_PAGE_PATH,
+ );
+ });
+});
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 2b60684e60a..2c712feac86 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
@@ -1,4 +1,12 @@
-import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlDisclosureDropdown,
+ GlFormCheckbox,
+ GlLoadingIcon,
+ GlModal,
+ GlKeysetPagination,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { stubComponent } from 'helpers/stub_component';
@@ -13,6 +21,7 @@ import {
packageFilesQuery,
packageDestroyFilesMutation,
packageDestroyFilesMutationError,
+ pagination,
} from 'jest/packages_and_registries/package_registry/mock_data';
import {
DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
@@ -22,16 +31,22 @@ import {
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
+import { NEXT, PREV } from '~/vue_shared/components/pagination/constants';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
+import { scrollToElement } from '~/lib/utils/common_utils';
import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql';
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
Vue.use(VueApollo);
jest.mock('~/alert');
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ scrollToElement: jest.fn(),
+}));
describe('Package Files', () => {
let wrapper;
@@ -43,13 +58,15 @@ describe('Package Files', () => {
const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findPackageFilesAlert = () => wrapper.findComponent(GlAlert);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link');
const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon);
const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip);
- const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown));
+ const findFirstActionMenu = () =>
+ extendedWrapper(findFirstRow().findComponent(GlDisclosureDropdown));
const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file');
- const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
+ const findFirstToggleDetailsButton = () => findFirstRow().findByTestId('toggle-details-button');
const findFirstRowShaComponent = (id) => wrapper.findByTestId(id);
const findCheckAllCheckbox = () => wrapper.findByTestId('package-files-checkbox-all');
const findAllRowCheckboxes = () => wrapper.findAllByTestId('package-files-checkbox');
@@ -68,6 +85,7 @@ describe('Package Files', () => {
stubs,
resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] })),
filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
+ options = {},
} = {}) => {
const requestHandlers = [
[getPackageFiles, resolver],
@@ -92,9 +110,14 @@ describe('Package Files', () => {
}),
...stubs,
},
+ ...options,
});
};
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
describe('rows', () => {
it('do not get rendered when query is loading', () => {
createComponent();
@@ -123,6 +146,7 @@ describe('Package Files', () => {
await waitForPromises();
expect(findPackageFilesAlert().exists()).toBe(false);
+ expect(Sentry.captureException).not.toHaveBeenCalled();
});
it('renders gl-alert if load fails', async () => {
@@ -133,6 +157,40 @@ describe('Package Files', () => {
expect(findPackageFilesAlert().text()).toBe(
s__('PackageRegistry|Something went wrong while fetching package assets.'),
);
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+
+ it('renders pagination', async () => {
+ createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) });
+ await waitForPromises();
+
+ const { endCursor, startCursor, hasNextPage, hasPreviousPage } = pagination();
+
+ expect(findPagination().props()).toMatchObject({
+ endCursor,
+ startCursor,
+ hasNextPage,
+ hasPreviousPage,
+ prevText: PREV,
+ nextText: NEXT,
+ disabled: false,
+ });
+ });
+
+ it('hides pagination when only one page', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageFilesQuery({
+ extendPagination: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ },
+ }),
+ ),
+ });
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(false);
});
});
@@ -204,7 +262,7 @@ describe('Package Files', () => {
expect(findFirstActionMenu().exists()).toBe(true);
expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v');
expect(findFirstActionMenu().props('textSrOnly')).toBe(true);
- expect(findFirstActionMenu().props('text')).toMatchInterpolatedText('More actions');
+ expect(findFirstActionMenu().props('toggleText')).toMatchInterpolatedText('More actions');
});
describe('menu items', () => {
@@ -214,7 +272,7 @@ describe('Package Files', () => {
});
it('shows delete file confirmation modal', async () => {
- await findActionMenuDelete().trigger('click');
+ await findActionMenuDelete().vm.$emit('action');
expect(showMock).toHaveBeenCalledTimes(1);
@@ -354,7 +412,7 @@ describe('Package Files', () => {
resolver: jest.fn().mockResolvedValue(
packageFilesQuery({
files: [file],
- pageInfo: {
+ extendPagination: {
hasNextPage: false,
},
}),
@@ -379,7 +437,7 @@ describe('Package Files', () => {
createComponent({
resolver: jest.fn().mockResolvedValue(
packageFilesQuery({
- pageInfo: {
+ extendPagination: {
hasNextPage: false,
},
}),
@@ -421,6 +479,69 @@ describe('Package Files', () => {
});
});
+ describe('when user interacts with pagination', () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+
+ beforeEach(async () => {
+ createComponent({ resolver, options: { attachTo: document.body } });
+ await waitForPromises();
+ });
+
+ describe('when list emits next event', () => {
+ beforeEach(() => {
+ findPagination().vm.$emit('next');
+ });
+
+ it('fetches the next set of files', () => {
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ after: pagination().endCursor,
+ first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
+ }),
+ );
+ });
+
+ it('scrolls to top of package files component', async () => {
+ await waitForPromises();
+
+ expect(scrollToElement).toHaveBeenCalledWith(wrapper.vm.$el);
+ });
+
+ it('first row is the active element', async () => {
+ await waitForPromises();
+
+ expect(findFirstRow().element).toBe(document.activeElement);
+ });
+ });
+
+ describe('when list emits prev event', () => {
+ beforeEach(() => {
+ findPagination().vm.$emit('prev');
+ });
+
+ it('fetches the previous set of files', () => {
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ before: pagination().startCursor,
+ last: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
+ }),
+ );
+ });
+
+ it('scrolls to top of package files component', async () => {
+ await waitForPromises();
+
+ expect(scrollToElement).toHaveBeenCalledWith(wrapper.vm.$el);
+ });
+
+ it('first row is the active element', async () => {
+ await waitForPromises();
+
+ expect(findFirstRow().element).toBe(document.activeElement);
+ });
+ });
+ });
+
describe('deleting a file', () => {
const doDeleteFile = async () => {
const first = findAllRowCheckboxes().at(0);
@@ -442,6 +563,7 @@ describe('Package Files', () => {
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPagination().props('disabled')).toBe(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
@@ -474,7 +596,7 @@ describe('Package Files', () => {
expect(resolver).toHaveBeenCalledTimes(2);
expect(resolver).toHaveBeenCalledWith({
id: '1',
- first: 100,
+ first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
});
});
@@ -534,6 +656,7 @@ describe('Package Files', () => {
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPagination().props('disabled')).toBe(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
@@ -566,7 +689,7 @@ describe('Package Files', () => {
expect(resolver).toHaveBeenCalledTimes(2);
expect(resolver).toHaveBeenCalledWith({
id: '1',
- first: 100,
+ first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
});
});
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 f7c8e909ff6..bc7203f73c9 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,5 +1,13 @@
-import { GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlFormCheckbox,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTruncate,
+} from '@gitlab/ui';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -24,10 +32,16 @@ describe('VersionRow', () => {
const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
- const findDeleteDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDeleteDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
- function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
- wrapper = shallowMountExtended(VersionRow, {
+ function createComponent(options = {}) {
+ const {
+ mountFn = shallowMountExtended,
+ packageEntity = packageVersion,
+ selected = false,
+ } = options;
+
+ wrapper = mountFn(VersionRow, {
propsData: {
packageEntity,
selected,
@@ -35,6 +49,7 @@ describe('VersionRow', () => {
stubs: {
GlSprintf,
GlTruncate,
+ GlDisclosureDropdown,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
@@ -100,9 +115,7 @@ describe('VersionRow', () => {
});
it('renders checkbox in selected state if selected', () => {
- createComponent({
- selected: true,
- });
+ createComponent({ selected: true });
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
expect(findListItem().props('selected')).toBe(true);
@@ -116,19 +129,16 @@ describe('VersionRow', () => {
expect(findDeleteDropdownItem().exists()).toBe(false);
});
- it('exists and has the correct props', () => {
+ it('exists', () => {
createComponent();
expect(findDeleteDropdownItem().exists()).toBe(true);
- expect(findDeleteDropdownItem().attributes()).toMatchObject({
- variant: 'danger',
- });
});
- it('emits the delete event when the delete button is clicked', () => {
- createComponent();
+ it('emits the delete event when the delete button is clicked', async () => {
+ createComponent({ mountFn: mountExtended });
- findDeleteDropdownItem().vm.$emit('click');
+ await findDeleteDropdownItem().find('button').trigger('click');
expect(wrapper.emitted('delete')).toHaveLength(1);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index c647230bc5f..0443fb85dc9 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -64,16 +64,12 @@ exports[`packages_list_row renders 1`] = `
withtooltip="true"
/>
- <!---->
-
<span
class="gl-ml-2"
data-testid="package-type"
>
· npm
</span>
-
- <!---->
</div>
</div>
</div>
@@ -91,15 +87,16 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<span
- data-testid="created-date"
+ data-testid="right-secondary"
>
- Created
- <timeago-tooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2020-08-17T14:23:32Z"
- tooltipplacement="top"
- />
+ Published
+ <time
+ class=""
+ datetime="2020-05-17T14:23:32Z"
+ title="May 17, 2020 2:23pm UTC"
+ >
+ 1 month ago
+ </time>
</span>
</div>
</div>
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 81ad47b1e13..523d5f855fc 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
@@ -5,9 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
-import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -29,14 +27,13 @@ describe('packages_list_row', () => {
const defaultProvide = {
isGroupPage: false,
+ canDeletePackages: true,
};
const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
- const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false };
const findPackageTags = () => wrapper.findComponent(PackageTags);
- const findPackagePath = () => wrapper.findComponent(PackagePath);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
const findPackageType = () => wrapper.findByTestId('package-type');
const findPackageLink = () => wrapper.findByTestId('details-link');
@@ -44,8 +41,7 @@ describe('packages_list_row', () => {
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const findPackageVersion = () => findLeftSecondaryInfos().findComponent(GlTruncate);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
- const findCreatedDateText = () => wrapper.findByTestId('created-date');
- const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findRightSecondary = () => wrapper.findByTestId('right-secondary');
const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate);
@@ -60,6 +56,7 @@ describe('packages_list_row', () => {
stubs: {
ListItem,
GlSprintf,
+ TimeagoTooltip,
},
propsData: {
packageEntity,
@@ -106,18 +103,11 @@ describe('packages_list_row', () => {
});
});
- describe('when it is group', () => {
- it('has a package path component', () => {
- mountComponent({ provide: { isGroupPage: true } });
-
- expect(findPackagePath().exists()).toBe(true);
- expect(findPackagePath().props()).toMatchObject({ path: 'gitlab-org/gitlab-test' });
- });
- });
-
describe('delete button', () => {
it('does not exist when package cannot be destroyed', () => {
- mountComponent({ packageEntity: packageCannotDestroy });
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, canDestroy: false },
+ });
expect(findDeleteDropdown().exists()).toBe(false);
});
@@ -180,7 +170,10 @@ describe('packages_list_row', () => {
describe('left action template', () => {
it('does not render checkbox if not permitted', () => {
mountComponent({
- packageEntity: { ...packageWithoutTags, canDestroy: false },
+ provide: {
+ ...defaultProvide,
+ canDeletePackages: false,
+ },
});
expect(findBulkDeleteAction().exists()).toBe(false);
@@ -223,14 +216,6 @@ describe('packages_list_row', () => {
});
});
- it('if the pipeline exists show the author message', () => {
- mountComponent({
- packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
- });
-
- expect(findLeftSecondaryInfos().text()).toContain('published by Administrator');
- });
-
it('has package type with middot', () => {
mountComponent();
@@ -247,13 +232,50 @@ describe('packages_list_row', () => {
expect(findPublishMethod().props('pipeline')).toEqual(packagePipelines()[0]);
});
- it('has the created date', () => {
- mountComponent();
+ it('if the package is published through CI show the author name', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
+ });
+
+ expect(findRightSecondary().text()).toBe(`Published by Administrator, 1 month ago`);
+ });
- expect(findCreatedDateText().text()).toMatchInterpolatedText(PackagesListRow.i18n.createdAt);
- expect(findTimeAgoTooltip().props()).toMatchObject({
- time: packageData().createdAt,
+ it('if the package is published manually then dont show author name', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags },
});
+
+ expect(findRightSecondary().text()).toBe(`Published 1 month ago`);
+ });
+ });
+
+ describe('right info for a group registry', () => {
+ it('if the package is published through CI show the project and author name', () => {
+ mountComponent({
+ provide: {
+ ...defaultProvide,
+ isGroupPage: true,
+ },
+ packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
+ });
+
+ expect(findRightSecondary().text()).toBe(
+ `Published to ${packageWithoutTags.project.name} by Administrator, 1 month ago`,
+ );
+ });
+
+ it('if the package is published manually dont show project and the author name', () => {
+ mountComponent({
+ provide: {
+ ...defaultProvide,
+ isGroupPage: true,
+ },
+ packageEntity: { ...packageWithoutTags },
+ });
+
+ expect(findRightSecondary().text()).toBe(
+ `Published to ${packageWithoutTags.project.name}, 1 month ago`,
+ );
});
});
});
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 483b7a9383d..fad8863e3d9 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
@@ -41,6 +41,10 @@ describe('packages_list', () => {
groupSettings: defaultPackageGroupSettings,
};
+ const defaultProvide = {
+ canDeletePackages: true,
+ };
+
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
@@ -52,8 +56,9 @@ describe('packages_list', () => {
const showMock = jest.fn();
- const mountComponent = (props) => {
+ const mountComponent = ({ props = {}, provide = defaultProvide } = {}) => {
wrapper = shallowMountExtended(PackagesList, {
+ provide,
propsData: {
...defaultProps,
...props,
@@ -75,7 +80,7 @@ describe('packages_list', () => {
describe('when is loading', () => {
beforeEach(() => {
- mountComponent({ isLoading: true });
+ mountComponent({ props: { isLoading: true } });
});
it('shows skeleton loader', () => {
@@ -109,6 +114,7 @@ describe('packages_list', () => {
title: '2 packages',
items: defaultProps.list,
pagination: defaultProps.pageInfo,
+ hiddenDelete: false,
isLoading: false,
});
});
@@ -137,6 +143,16 @@ describe('packages_list', () => {
});
});
+ describe('when the user does not have permission to destroy packages', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { canDeletePackages: false } });
+ });
+
+ it('sets the hidden delete prop of registry list to true', () => {
+ expect(findRegistryList().props('hiddenDelete')).toBe(true);
+ });
+ });
+
describe.each`
description | finderFunction | deletePayload
${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage}
@@ -262,7 +278,7 @@ describe('packages_list', () => {
describe('when an error package is present', () => {
beforeEach(() => {
- mountComponent({ list: [firstPackage, errorPackage] });
+ mountComponent({ props: { list: [firstPackage, errorPackage] } });
return nextTick();
});
@@ -290,7 +306,7 @@ describe('packages_list', () => {
describe('when the list is empty', () => {
beforeEach(() => {
- mountComponent({ list: [] });
+ mountComponent({ props: { list: [] } });
});
it('show the empty slot', () => {
@@ -301,7 +317,7 @@ describe('packages_list', () => {
describe('pagination', () => {
beforeEach(() => {
- mountComponent({ pageInfo: { hasPreviousPage: true } });
+ mountComponent({ props: { pageInfo: { hasPreviousPage: true } } });
});
it('emits prev-page events when the prev event is fired', () => {
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 6995a4cc635..91dc02f8f39 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -9,7 +9,7 @@ export const packageTags = () => [
export const packagePipelines = (extend) => [
{
commitPath: '/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
id: 'gid://gitlab/Ci::Pipeline/36',
path: '/namespace14/project14/-/pipelines/36',
name: 'project14',
@@ -38,7 +38,7 @@ export const packageFiles = () => [
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad',
fileSha256: 'fileSha256',
size: '409600',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
downloadPath: 'downloadPath',
__typename: 'PackageFile',
},
@@ -49,7 +49,7 @@ export const packageFiles = () => [
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss',
fileSha256: null,
size: '409600',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
downloadPath: 'downloadPath',
__typename: 'PackageFile',
},
@@ -92,6 +92,7 @@ export const dependencyLinks = () => [
export const packageProject = () => ({
id: '1',
+ name: 'gitlab-test',
fullPath: 'gitlab-org/gitlab-test',
webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-test',
__typename: 'Project',
@@ -144,7 +145,7 @@ export const packageData = (extend) => ({
name: '@gitlab-org/package-15',
packageType: 'NPM',
version: '1.0.0',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
lastDownloadedAt: '2021-08-17T14:23:32Z',
status: 'DEFAULT',
@@ -278,15 +279,12 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({
},
});
-export const packageFilesQuery = ({ files = packageFiles(), pageInfo = {} } = {}) => ({
+export const packageFilesQuery = ({ files = packageFiles(), extendPagination = {} } = {}) => ({
data: {
package: {
id: 'gid://gitlab/Packages::Package/111',
packageFiles: {
- pageInfo: {
- hasNextPage: true,
- ...pageInfo,
- },
+ pageInfo: pagination(extendPagination),
nodes: files,
__typename: 'PackageFileConnection',
},
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 2ee24200ed3..0d262036ee7 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
@@ -219,7 +219,11 @@ describe('PackagesListApp', () => {
await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
- expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
+ expect.objectContaining({
+ first: null,
+ before: pagination().startCursor,
+ last: GRAPHQL_PAGE_SIZE,
+ }),
);
});
});
diff --git a/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
index b1d2e443d54..d90393d8ab3 100644
--- a/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
@@ -1,10 +1,11 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
+import { setVueErrorHandler } from '../../../../__helpers__/set_vue_error_handler';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -45,8 +46,6 @@ describe('Cancel jobs modal', () => {
});
it('displays error if canceling jobs failed', async () => {
- Vue.config.errorHandler = () => {}; // silencing thrown error
-
const dummyError = new Error('canceling jobs failed');
// TODO: We can't use axios-mock-adapter because our current version
// does not support responseURL
@@ -57,6 +56,7 @@ describe('Cancel jobs modal', () => {
return Promise.reject(dummyError);
});
+ setVueErrorHandler({ instance: wrapper.vm, handler: () => {} }); // silencing thrown error
wrapper.findComponent(GlModal).vm.$emit('primary');
await nextTick();
diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js
index 19240f1a044..baf0ca2beca 100644
--- a/spec/frontend/pages/groups/new/components/app_spec.js
+++ b/spec/frontend/pages/groups/new/components/app_spec.js
@@ -1,4 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import GROUP_IMPORT_SVG_URL from '@gitlab/svgs/dist/illustrations/group-import.svg?url';
+import GROUP_NEW_SVG_URL from '@gitlab/svgs/dist/illustrations/group-new.svg?url';
+
import App from '~/pages/groups/new/components/app.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
@@ -27,6 +30,7 @@ describe('App component', () => {
{ href: '#', text: 'New group' },
]);
expect(findCreateGroupPanel().title).toBe('Create group');
+ expect(findCreateGroupPanel().imageSrc).toBe(GROUP_NEW_SVG_URL);
});
it('creates correct component for subgroup creation', () => {
@@ -45,5 +49,6 @@ describe('App component', () => {
]);
expect(findCreateGroupPanel().title).toBe('Create subgroup');
expect(findCreateGroupPanel().detailProps).toEqual(detailProps);
+ expect(findCreateGroupPanel().imageSrc).toBe(GROUP_IMPORT_SVG_URL);
});
});
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 07d05293a3c..197a76f2c86 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
@@ -219,4 +219,17 @@ describe('Interval Pattern Input Component', () => {
expect(findIcon().exists()).toBe(false);
});
});
+
+ describe('cronValue event', () => {
+ it('emits cronValue event with cron value', async () => {
+ createWrapper();
+
+ findCustomInput().element.value = '0 16 * * *';
+ findCustomInput().trigger('input');
+
+ await nextTick();
+
+ expect(wrapper.emitted()).toEqual({ cronValue: [['0 16 * * *']] });
+ });
+ });
});
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 1a3eb86a00e..db889abad88 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -7,16 +7,11 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import {
- CONTENT_EDITOR_LOADED_ACTION,
- SAVED_USING_CONTENT_EDITOR_ACTION,
- WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- WIKI_FORMAT_LABEL,
- WIKI_FORMAT_UPDATED_ACTION,
-} from '~/pages/shared/wikis/constants';
+import { WIKI_FORMAT_LABEL, WIKI_FORMAT_UPDATED_ACTION } from '~/pages/shared/wikis/constants';
import { DRAWIO_ORIGIN } from 'spec/test_constants';
jest.mock('~/emoji');
+jest.mock('~/lib/graphql');
describe('WikiForm', () => {
let wrapper;
@@ -94,6 +89,15 @@ describe('WikiForm', () => {
GlFormInput,
GlFormGroup,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
}),
);
}
@@ -224,7 +228,22 @@ describe('WikiForm', () => {
});
it('triggers wiki format tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'wiki_format_updated', {
+ extra: {
+ old_format: 'markdown',
+ project_path: '/project/path/-/wikis/home',
+ value: 'markdown',
+ },
+ label: 'wiki_format',
+ });
+ });
+
+ it('tracks editor type used', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Wiki',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
});
it('does not trim page content', () => {
@@ -306,12 +325,6 @@ describe('WikiForm', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
- it('sends tracking event when editor loads', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
- });
-
describe('when triggering form submit', () => {
const updatedMarkdown = 'hello **world**';
@@ -321,10 +334,6 @@ describe('WikiForm', () => {
});
it('triggers tracking events on form submit', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
-
expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
extra: {
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index 7095525e948..bb9a4b85e0e 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -141,10 +141,6 @@ describe('Pipeline Wizard - Commit Page', () => {
it('emits a done event', () => {
expect(wrapper.emitted().done.length).toBe(1);
});
-
- afterEach(() => {
- jest.clearAllMocks();
- });
});
describe('failed commit', () => {
@@ -167,10 +163,6 @@ describe('Pipeline Wizard - Commit Page', () => {
it('will not emit a done event', () => {
expect(wrapper.emitted().done?.length).toBeUndefined();
});
-
- afterEach(() => {
- jest.clearAllMocks();
- });
});
});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index 6d7d4363189..2284c875f58 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -1,42 +1,58 @@
import { mount } from '@vue/test-utils';
import { Document } from 'yaml';
import YamlEditor from '~/pipeline_wizard/components/editor.vue';
+import SourceEditor from '~/editor/source_editor';
describe('Pages Yaml Editor wrapper', () => {
let wrapper;
+ const defaultDoc = new Document({ foo: 'bar' });
+
const defaultOptions = {
- propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
+ propsData: { doc: defaultDoc, filename: 'foo.yml' },
+ };
+
+ const getLatestValue = () => {
+ const latest = wrapper.emitted('update:yaml').pop();
+ return latest[0];
};
describe('mount hook', () => {
beforeEach(() => {
+ jest.spyOn(SourceEditor.prototype, 'createInstance');
+
wrapper = mount(YamlEditor, defaultOptions);
});
- it('editor is mounted', () => {
- expect(wrapper.vm.editor).not.toBeUndefined();
- expect(wrapper.find('.gl-source-editor').exists()).toBe(true);
+ it('creates a source editor instance', () => {
+ expect(SourceEditor.prototype.createInstance).toHaveBeenCalledWith({
+ el: wrapper.element,
+ blobPath: 'foo.yml',
+ language: 'yaml',
+ });
+ });
+
+ it('editor is mounted in the wrapper', () => {
+ expect(wrapper.find('.gl-source-editor.monaco-editor').exists()).toBe(true);
+ });
+
+ it("causes the editor's value to be set to the stringified document", () => {
+ expect(getLatestValue()).toEqual(defaultDoc.toString());
});
});
describe('watchers', () => {
+ beforeEach(() => {
+ wrapper = mount(YamlEditor, defaultOptions);
+ });
+
describe('doc', () => {
const doc = new Document({ baz: ['bar'] });
- beforeEach(() => {
- wrapper = mount(YamlEditor, defaultOptions);
- });
-
- it("causes the editor's value to be set to the stringified document", async () => {
- await wrapper.setProps({ doc });
- expect(wrapper.vm.editor.getValue()).toEqual(doc.toString());
- });
-
it('emits an update:yaml event with the yaml representation of doc', async () => {
await wrapper.setProps({ doc });
- const changeEvents = wrapper.emitted('update:yaml');
- expect(changeEvents[2]).toEqual([doc.toString()]);
+
+ expect(getLatestValue()).toEqual(doc.toString());
});
it('does not cause the touch event to be emitted', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js
new file mode 100644
index 00000000000..4ba1b82e971
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js
@@ -0,0 +1,252 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlIcon, GlLink } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue';
+import RetryMrFailedJobMutation from '~/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql';
+import { job } from './mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+const createFakeEvent = () => ({ stopPropagation: jest.fn() });
+
+describe('FailedJobDetails component', () => {
+ let wrapper;
+ let mockRetryResponse;
+
+ const retrySuccessResponse = {
+ data: {
+ jobRetry: {
+ errors: [],
+ },
+ },
+ };
+
+ const defaultProps = {
+ job,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [[RetryMrFailedJobMutation, mockRetryResponse]];
+
+ wrapper = shallowMountExtended(FailedJobDetails, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ apolloProvider: createMockApollo(handlers),
+ });
+ };
+
+ const findArrowIcon = () => wrapper.findComponent(GlIcon);
+ const findJobId = () => wrapper.findComponent(GlLink);
+ const findHiddenJobLog = () => wrapper.findByTestId('log-is-hidden');
+ const findVisibleJobLog = () => wrapper.findByTestId('log-is-visible');
+ const findJobName = () => wrapper.findByText(defaultProps.job.name);
+ const findRetryButton = () => wrapper.findByLabelText('Retry');
+ const findRow = () => wrapper.findByTestId('widget-row');
+ const findStageName = () => wrapper.findByText(defaultProps.job.stage.name);
+
+ beforeEach(() => {
+ mockRetryResponse = jest.fn();
+ mockRetryResponse.mockResolvedValue(retrySuccessResponse);
+ });
+
+ describe('ui', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the job name', () => {
+ expect(findJobName().exists()).toBe(true);
+ });
+
+ it('renders the stage name', () => {
+ expect(findStageName().exists()).toBe(true);
+ });
+
+ it('renders the job id as a link', () => {
+ const jobId = getIdFromGraphQLId(defaultProps.job.id);
+
+ expect(findJobId().exists()).toBe(true);
+ expect(findJobId().text()).toContain(String(jobId));
+ });
+
+ it('does not renders the job lob', () => {
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ });
+ });
+
+ describe('Retry action', () => {
+ describe('when the job is not retryable', () => {
+ beforeEach(() => {
+ createComponent({ props: { job: { ...job, retryable: false } } });
+ });
+
+ it('disables the retry button', () => {
+ expect(findRetryButton().props().disabled).toBe(true);
+ });
+ });
+
+ describe('when the job is retryable', () => {
+ describe('and user has permission to update the build', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('enables the retry button', () => {
+ expect(findRetryButton().props().disabled).toBe(false);
+ });
+
+ describe('when clicking on the retry button', () => {
+ it('passes the loading state to the button', async () => {
+ await findRetryButton().vm.$emit('click', createFakeEvent());
+
+ expect(findRetryButton().props().loading).toBe(true);
+ });
+
+ describe('and it succeeds', () => {
+ beforeEach(async () => {
+ findRetryButton().vm.$emit('click', createFakeEvent());
+ await waitForPromises();
+ });
+
+ it('is no longer loading', () => {
+ expect(findRetryButton().props().loading).toBe(false);
+ });
+
+ it('calls the retry mutation', () => {
+ expect(mockRetryResponse).toHaveBeenCalled();
+ expect(mockRetryResponse).toHaveBeenCalledWith({
+ id: job.id,
+ });
+ });
+
+ it('emits the `retried-job` event', () => {
+ expect(wrapper.emitted('job-retried')).toStrictEqual([[job.name]]);
+ });
+ });
+
+ describe('and it fails', () => {
+ const customErrorMsg = 'Custom error message from API';
+
+ beforeEach(async () => {
+ mockRetryResponse.mockResolvedValue({
+ data: { jobRetry: { errors: [customErrorMsg] } },
+ });
+ findRetryButton().vm.$emit('click', createFakeEvent());
+
+ await waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: customErrorMsg });
+ });
+
+ it('does not emits the `refetch-jobs` event', () => {
+ expect(wrapper.emitted('refetch-jobs')).toBeUndefined();
+ });
+ });
+ });
+ });
+
+ describe('and user does not have permission to update the build', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { job: { ...job, retryable: true, userPermissions: { updateBuild: false } } },
+ });
+ });
+
+ it('disables the retry button', () => {
+ expect(findRetryButton().props().disabled).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('Job log', () => {
+ describe('without permissions', () => {
+ beforeEach(async () => {
+ createComponent({ props: { job: { ...job, userPermissions: { readBuild: false } } } });
+ await findRow().trigger('click');
+ });
+
+ it('does not renders the received html of the job log', () => {
+ expect(findVisibleJobLog().html()).not.toContain(defaultProps.job.trace.htmlSummary);
+ });
+
+ it('shows a permission error message', () => {
+ expect(findVisibleJobLog().text()).toBe(
+ "You do not have permission to read this job's log",
+ );
+ });
+ });
+
+ describe('with permissions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when clicking on the row', () => {
+ beforeEach(async () => {
+ await findRow().trigger('click');
+ });
+
+ describe('while collapsed', () => {
+ it('expands the job log', () => {
+ expect(findHiddenJobLog().exists()).toBe(false);
+ expect(findVisibleJobLog().exists()).toBe(true);
+ });
+
+ it('renders the down arrow', () => {
+ expect(findArrowIcon().props().name).toBe('chevron-down');
+ });
+
+ it('renders the received html of the job log', () => {
+ expect(findVisibleJobLog().html()).toContain(defaultProps.job.trace.htmlSummary);
+ });
+ });
+
+ describe('while expanded', () => {
+ it('collapes the job log', async () => {
+ expect(findHiddenJobLog().exists()).toBe(false);
+ expect(findVisibleJobLog().exists()).toBe(true);
+
+ await findRow().trigger('click');
+
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ });
+
+ it('renders the right arrow', async () => {
+ expect(findArrowIcon().props().name).toBe('chevron-down');
+
+ await findRow().trigger('click');
+
+ expect(findArrowIcon().props().name).toBe('chevron-right');
+ });
+ });
+ });
+
+ describe('when clicking on a link element within the row', () => {
+ it('does not expands/collapse the job log', async () => {
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ expect(findArrowIcon().props().name).toBe('chevron-right');
+
+ await findJobId().vm.$emit('click');
+
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ expect(findArrowIcon().props().name).toBe('chevron-right');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js
new file mode 100644
index 00000000000..fc8263c6c4d
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js
@@ -0,0 +1,236 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { GlLoadingIcon, GlToast } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue';
+import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue';
+import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils';
+import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql';
+import { failedJobsMock, failedJobsMock2, failedJobsMockEmpty, activeFailedJobsMock } from './mock';
+
+Vue.use(VueApollo);
+Vue.use(GlToast);
+
+jest.mock('~/alert');
+
+describe('FailedJobsList component', () => {
+ let wrapper;
+ let mockFailedJobsResponse;
+ const showToast = jest.fn();
+
+ const defaultProps = {
+ graphqlResourceEtag: 'api/graphql',
+ isPipelineActive: false,
+ pipelineIid: 1,
+ pipelinePath: '/pipelines/1',
+ };
+
+ const defaultProvide = {
+ fullPath: 'namespace/project/',
+ graphqlPath: 'api/graphql',
+ };
+
+ const createComponent = ({ props = {}, provide } = {}) => {
+ const handlers = [[getPipelineFailedJobs, mockFailedJobsResponse]];
+ const mockApollo = createMockApollo(handlers);
+
+ wrapper = shallowMountExtended(FailedJobsList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ apolloProvider: mockApollo,
+ mocks: {
+ $toast: {
+ show: showToast,
+ },
+ },
+ });
+ };
+
+ const findAllHeaders = () => wrapper.findAllByTestId('header');
+ const findFailedJobRows = () => wrapper.findAllComponents(FailedJobDetails);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findNoFailedJobsText = () => wrapper.findByText('No failed jobs in this pipeline 🎉');
+
+ beforeEach(() => {
+ mockFailedJobsResponse = jest.fn();
+ });
+
+ describe('when loading failed jobs', () => {
+ beforeEach(() => {
+ mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
+ createComponent();
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when failed jobs have loaded', () => {
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
+ jest.spyOn(utils, 'sortJobsByStatus');
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not renders a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders table column', () => {
+ expect(findAllHeaders()).toHaveLength(4);
+ });
+
+ it('shows the list of failed jobs', () => {
+ expect(findFailedJobRows()).toHaveLength(
+ failedJobsMock.data.project.pipeline.jobs.nodes.length,
+ );
+ });
+
+ it('does not renders the empty state', () => {
+ expect(findNoFailedJobsText().exists()).toBe(false);
+ });
+
+ it('calls sortJobsByStatus', () => {
+ expect(utils.sortJobsByStatus).toHaveBeenCalledWith(
+ failedJobsMock.data.project.pipeline.jobs.nodes,
+ );
+ });
+ });
+
+ describe('when there are no failed jobs', () => {
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockResolvedValue(failedJobsMockEmpty);
+ jest.spyOn(utils, 'sortJobsByStatus');
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('renders the empty state', () => {
+ expect(findNoFailedJobsText().exists()).toBe(true);
+ });
+ });
+
+ describe('polling', () => {
+ it.each`
+ isGraphqlActive | text
+ ${true} | ${'polls'}
+ ${false} | ${'does not poll'}
+ `(`$text when isGraphqlActive: $isGraphqlActive`, async ({ isGraphqlActive }) => {
+ const defaultCount = 2;
+ const newCount = 1;
+
+ const expectedCount = isGraphqlActive ? newCount : defaultCount;
+ const expectedCallCount = isGraphqlActive ? 2 : 1;
+ const mockResponse = isGraphqlActive ? activeFailedJobsMock : failedJobsMock;
+
+ // Second result is to simulate polling with a different response
+ mockFailedJobsResponse.mockResolvedValueOnce(mockResponse);
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
+
+ createComponent();
+ await waitForPromises();
+
+ // Initially, we get the first response which is always the default
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
+ expect(findFailedJobRows()).toHaveLength(defaultCount);
+
+ jest.advanceTimersByTime(10000);
+ await waitForPromises();
+
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(expectedCallCount);
+ expect(findFailedJobRows()).toHaveLength(expectedCount);
+ });
+ });
+
+ describe('when a REST action occurs', () => {
+ beforeEach(() => {
+ // Second result is to simulate polling with a different response
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock);
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
+ });
+
+ it.each([true, false])('triggers a refetch of the jobs count', async (isPipelineActive) => {
+ const defaultCount = 2;
+ const newCount = 1;
+
+ createComponent({ props: { isPipelineActive } });
+ await waitForPromises();
+
+ // Initially, we get the first response which is always the default
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
+ expect(findFailedJobRows()).toHaveLength(defaultCount);
+
+ wrapper.setProps({ isPipelineActive: !isPipelineActive });
+ await waitForPromises();
+
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(2);
+ expect(findFailedJobRows()).toHaveLength(newCount);
+ });
+ });
+
+ describe('when an error occurs loading jobs', () => {
+ const errorMessage = "We couldn't fetch jobs for you because you are not qualified";
+
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockRejectedValue({ message: errorMessage });
+
+ createComponent();
+
+ await waitForPromises();
+ });
+ it('does not renders a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('calls create Alert with the error message and danger variant', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+
+ describe('when `refetch-jobs` job is fired from the widget', () => {
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock);
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('refetches all failed jobs', async () => {
+ expect(findFailedJobRows()).not.toHaveLength(
+ failedJobsMock2.data.project.pipeline.jobs.nodes.length,
+ );
+
+ await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name');
+ await waitForPromises();
+
+ expect(findFailedJobRows()).toHaveLength(
+ failedJobsMock2.data.project.pipeline.jobs.nodes.length,
+ );
+ });
+
+ it('shows a toast message', async () => {
+ await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name');
+ await waitForPromises();
+
+ expect(showToast).toHaveBeenCalledWith('job-name job is being retried');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
index a4c90fa3876..b047b57fc34 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
@@ -13,13 +13,17 @@ export const job = {
},
name: 'job-name',
retried: false,
+ retryable: true,
stage: {
id: '1',
name: 'build',
},
trace: {
- htmlSummary:
- '<span>To install the missing version, run `gem install bundler:2.4.13`<br/>\tfrom /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path\'<br/>\tfrom /usr/bin/bundle:23:in `&lt;main>\'<br/></span><div class="section-start" data-timestamp="1685044123" data-section="upload-artifacts-on-failure" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-upload-artifacts-on-failure">Uploading artifacts for failed job</span><span class="section section-header js-s-upload-artifacts-on-failure"><br/></span><span class="term-fg-l-green term-bold section line js-s-upload-artifacts-on-failure">Uploading artifacts...</span><span class="section line js-s-upload-artifacts-on-failure"><br/>Runtime platform </span><span class="section line js-s-upload-artifacts-on-failure"> arch</span><span class="section line js-s-upload-artifacts-on-failure">=arm64 os</span><span class="section line js-s-upload-artifacts-on-failure">=darwin pid</span><span class="section line js-s-upload-artifacts-on-failure">=16706 revision</span><span class="section line js-s-upload-artifacts-on-failure">=43b2dc3d version</span><span class="section line js-s-upload-artifacts-on-failure">=15.4.0<br/></span><span class="term-fg-yellow section line js-s-upload-artifacts-on-failure">WARNING: rspec.xml: no matching files. Ensure that the artifact path is relative to the working directory</span><span class="section line js-s-upload-artifacts-on-failure"> <br/></span><span class="term-fg-l-red term-bold section line js-s-upload-artifacts-on-failure">ERROR: No files to upload </span><span class="section line js-s-upload-artifacts-on-failure"> <br/></span><div class="section-end" data-section="upload-artifacts-on-failure"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit status 1<br/></span><span><br/></span>',
+ htmlSummary: '<h1>Hello</h1>',
+ },
+ userPermissions: {
+ readBuild: true,
+ updateBuild: true,
},
webPath: '/',
};
@@ -30,16 +34,44 @@ export const allowedToFailJob = {
allowFailure: true,
};
-export const failedJobsMock = {
- data: {
- project: {
- id: 'gid://gitlab/Project/20',
- pipeline: {
- id: 'gid://gitlab/Pipeline/20',
- jobs: {
- nodes: [allowedToFailJob, job],
+export const createFailedJobsMockCount = ({ count = 4, active = false } = {}) => {
+ return {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Pipeline/20',
+ active,
+ jobs: {
+ count,
+ },
},
},
},
- },
+ };
+};
+
+const createFailedJobsMock = (nodes, active = false) => {
+ return {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ active,
+ id: 'gid://gitlab/Pipeline/20',
+ jobs: {
+ count: nodes.length,
+ nodes,
+ },
+ },
+ },
+ },
+ };
};
+
+export const failedJobsMock = createFailedJobsMock([allowedToFailJob, job]);
+export const failedJobsMockEmpty = createFailedJobsMock([]);
+
+export const activeFailedJobsMock = createFailedJobsMock([allowedToFailJob, job], true);
+
+export const failedJobsMock2 = createFailedJobsMock([job]);
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
index df6d114f683..c1a885391e9 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
@@ -1,25 +1,16 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-
-import { GlButton, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { GlButton, GlIcon, GlPopover } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import { createAlert } from '~/alert';
-import WidgetFailedJobRow from '~/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue';
-import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils';
-import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql';
-import { failedJobsMock } from './mock';
+import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue';
-Vue.use(VueApollo);
jest.mock('~/alert');
describe('PipelineFailedJobsWidget component', () => {
let wrapper;
- let mockFailedJobsResponse;
const defaultProps = {
+ failedJobsCount: 4,
+ isPipelineActive: false,
pipelineIid: 1,
pipelinePath: '/pipelines/1',
};
@@ -28,10 +19,7 @@ describe('PipelineFailedJobsWidget component', () => {
fullPath: 'namespace/project/',
};
- const createComponent = ({ props = {}, provide } = {}) => {
- const handlers = [[getPipelineFailedJobs, mockFailedJobsResponse]];
- const mockApollo = createMockApollo(handlers);
-
+ const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = shallowMountExtended(PipelineFailedJobsWidget, {
propsData: {
...defaultProps,
@@ -41,29 +29,35 @@ describe('PipelineFailedJobsWidget component', () => {
...defaultProvide,
...provide,
},
- apolloProvider: mockApollo,
});
};
- const findAllHeaders = () => wrapper.findAllByTestId('header');
const findFailedJobsButton = () => wrapper.findComponent(GlButton);
- const findFailedJobRows = () => wrapper.findAllComponents(WidgetFailedJobRow);
+ const findFailedJobsList = () => wrapper.findAllComponents(FailedJobsList);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
const findInfoPopover = () => wrapper.findComponent(GlPopover);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- beforeEach(() => {
- mockFailedJobsResponse = jest.fn();
+ describe('when there are no failed jobs', () => {
+ beforeEach(() => {
+ createComponent({ props: { failedJobsCount: 0 } });
+ });
+
+ it('renders the show failed jobs button with a count of 0', () => {
+ expect(findFailedJobsButton().exists()).toBe(true);
+ expect(findFailedJobsButton().text()).toBe('Show failed jobs (0)');
+ });
});
- describe('ui', () => {
+ describe('when there are failed jobs', () => {
beforeEach(() => {
createComponent();
});
- it('renders the show failed jobs button', () => {
+ it('renders the show failed jobs button with correct count', () => {
expect(findFailedJobsButton().exists()).toBe(true);
- expect(findFailedJobsButton().text()).toBe('Show failed jobs');
+ expect(findFailedJobsButton().text()).toBe(
+ `Show failed jobs (${defaultProps.failedJobsCount})`,
+ );
});
it('renders the info icon', () => {
@@ -74,71 +68,53 @@ describe('PipelineFailedJobsWidget component', () => {
expect(findInfoPopover().exists()).toBe(true);
});
- it('does not show the list of failed jobs', () => {
- expect(findFailedJobRows()).toHaveLength(0);
+ it('does not render the failed jobs widget', () => {
+ expect(findFailedJobsList().exists()).toBe(false);
});
});
- describe('when loading failed jobs', () => {
+ describe('when the job button is clicked', () => {
beforeEach(async () => {
- mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
createComponent();
await findFailedJobsButton().vm.$emit('click');
});
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
+ it('renders the failed jobs widget', () => {
+ expect(findFailedJobsList().exists()).toBe(true);
});
});
- describe('when failed jobs have loaded', () => {
- beforeEach(async () => {
- mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
- jest.spyOn(utils, 'sortJobsByStatus');
-
+ describe('when the job count changes', () => {
+ beforeEach(() => {
createComponent();
-
- await findFailedJobsButton().vm.$emit('click');
- await waitForPromises();
- });
- it('does not renders a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
});
- it('renders table column', () => {
- expect(findAllHeaders()).toHaveLength(3);
- });
+ describe('from the prop', () => {
+ it('updates the job count', async () => {
+ const newJobCount = 12;
- it('shows the list of failed jobs', () => {
- expect(findFailedJobRows()).toHaveLength(
- failedJobsMock.data.project.pipeline.jobs.nodes.length,
- );
- });
+ expect(findFailedJobsButton().text()).toContain(String(defaultProps.failedJobsCount));
- it('calls sortJobsByStatus', () => {
- expect(utils.sortJobsByStatus).toHaveBeenCalledWith(
- failedJobsMock.data.project.pipeline.jobs.nodes,
- );
+ await wrapper.setProps({ failedJobsCount: newJobCount });
+
+ expect(findFailedJobsButton().text()).toContain(String(newJobCount));
+ });
});
- });
- describe('when an error occurs loading jobs', () => {
- const errorMessage = "We couldn't fetch jobs for you because you are not qualified";
+ describe('from the event', () => {
+ beforeEach(async () => {
+ await findFailedJobsButton().vm.$emit('click');
+ });
- beforeEach(async () => {
- mockFailedJobsResponse.mockRejectedValue({ message: errorMessage });
+ it('updates the job count', async () => {
+ const newJobCount = 12;
- createComponent();
+ expect(findFailedJobsButton().text()).toContain(String(defaultProps.failedJobsCount));
- await findFailedJobsButton().vm.$emit('click');
- await waitForPromises();
- });
- it('does not renders a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
+ await findFailedJobsList().at(0).vm.$emit('failed-jobs-count', newJobCount);
- it('calls create Alert with the error message and danger variant', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ expect(findFailedJobsButton().text()).toContain(String(newJobCount));
+ });
});
});
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js
deleted file mode 100644
index dfc2806840f..00000000000
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import { GlIcon, GlLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import WidgetFailedJobRow from '~/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue';
-
-describe('WidgetFailedJobRow component', () => {
- let wrapper;
-
- const defaultProps = {
- job: {
- id: 'gid://gitlab/Ci::Build/5240',
- detailedStatus: {
- group: 'running',
- icon: 'icon_status_running',
- },
- name: 'my-job',
- stage: {
- name: 'build',
- },
- trace: {
- htmlSummary: '<h1>job log</h1>',
- },
- webpath: '/',
- },
- };
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(WidgetFailedJobRow, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- const findArrowIcon = () => wrapper.findComponent(GlIcon);
- const findJobCiStatus = () => wrapper.findComponent(CiIcon);
- const findJobId = () => wrapper.findComponent(GlLink);
- const findHiddenJobLog = () => wrapper.findByTestId('log-is-hidden');
- const findVisibleJobLog = () => wrapper.findByTestId('log-is-visible');
- const findJobName = () => wrapper.findByText(defaultProps.job.name);
- const findRow = () => wrapper.findByTestId('widget-row');
- const findStageName = () => wrapper.findByText(defaultProps.job.stage.name);
-
- describe('ui', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders the job name', () => {
- expect(findJobName().exists()).toBe(true);
- });
-
- it('renders the stage name', () => {
- expect(findStageName().exists()).toBe(true);
- });
-
- it('renders the job id as a link', () => {
- const jobId = getIdFromGraphQLId(defaultProps.job.id);
-
- expect(findJobId().exists()).toBe(true);
- expect(findJobId().text()).toContain(String(jobId));
- });
-
- it('renders the ci status badge', () => {
- expect(findJobCiStatus().exists()).toBe(true);
- });
-
- it('renders the right arrow', () => {
- expect(findArrowIcon().props().name).toBe('chevron-right');
- });
-
- it('does not renders the job lob', () => {
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- });
- });
-
- describe('Job log', () => {
- beforeEach(() => {
- createComponent();
- });
-
- describe('when clicking on the row', () => {
- beforeEach(async () => {
- await findRow().trigger('click');
- });
-
- describe('while collapsed', () => {
- it('expands the job log', () => {
- expect(findHiddenJobLog().exists()).toBe(false);
- expect(findVisibleJobLog().exists()).toBe(true);
- });
-
- it('renders the down arrow', () => {
- expect(findArrowIcon().props().name).toBe('chevron-down');
- });
-
- it('renders the received html', () => {
- expect(findVisibleJobLog().html()).toContain(defaultProps.job.trace.htmlSummary);
- });
- });
-
- describe('while expanded', () => {
- it('collapes the job log', async () => {
- expect(findHiddenJobLog().exists()).toBe(false);
- expect(findVisibleJobLog().exists()).toBe(true);
-
- await findRow().trigger('click');
-
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- });
-
- it('renders the right arrow', async () => {
- expect(findArrowIcon().props().name).toBe('chevron-down');
-
- await findRow().trigger('click');
-
- expect(findArrowIcon().props().name).toBe('chevron-right');
- });
- });
- });
-
- describe('when clicking on a link element within the row', () => {
- it('does not expands/collapse the job log', async () => {
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- expect(findArrowIcon().props().name).toBe('chevron-right');
-
- await findJobId().vm.$emit('click');
-
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- expect(findArrowIcon().props().name).toBe('chevron-right');
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 9599b5e6b7b..7b59d82ae6f 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -34,7 +34,11 @@ import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_head
import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
-import { mapCallouts, mockCalloutsResponse } from './mock_data';
+import {
+ mapCallouts,
+ mockCalloutsResponse,
+ mockPipelineResponseWithTooManyJobs,
+} from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@@ -49,7 +53,10 @@ describe('Pipeline graph wrapper', () => {
let wrapper;
let requestHandlers;
- const findAlert = () => wrapper.findComponent(GlAlert);
+ let pipelineDetailsHandler;
+
+ const findAlert = () => wrapper.findByTestId('error-alert');
+ const findJobCountWarning = () => wrapper.findByTestId('job-count-warning');
const findDependenciesToggle = () => wrapper.findByTestId('show-links-toggle');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
@@ -83,7 +90,6 @@ describe('Pipeline graph wrapper', () => {
const createComponentWithApollo = ({
calloutsList = [],
data = {},
- getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
mountFn = shallowMountExtended,
provide = {},
} = {}) => {
@@ -92,7 +98,7 @@ describe('Pipeline graph wrapper', () => {
requestHandlers = {
getUserCalloutsHandler: jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)),
getPipelineHeaderDataHandler: jest.fn().mockResolvedValue(mockRunningPipelineHeaderData),
- getPipelineDetailsHandler,
+ getPipelineDetailsHandler: pipelineDetailsHandler,
};
const handlers = [
@@ -105,24 +111,29 @@ describe('Pipeline graph wrapper', () => {
createComponent({ apolloProvider, data, provide, mountFn });
};
+ beforeEach(() => {
+ pipelineDetailsHandler = jest.fn();
+ pipelineDetailsHandler.mockResolvedValue(mockPipelineResponse);
+ });
+
describe('when data is loading', () => {
- it('displays the loading icon', () => {
+ beforeEach(() => {
createComponentWithApollo();
+ });
+
+ it('displays the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not display the alert', () => {
- createComponentWithApollo();
expect(findAlert().exists()).toBe(false);
});
it('does not display the graph', () => {
- createComponentWithApollo();
expect(findGraph().exists()).toBe(false);
});
it('skips querying headerPipeline', () => {
- createComponentWithApollo();
expect(wrapper.vm.$apollo.queries.headerPipeline.skip).toBe(true);
});
});
@@ -153,11 +164,25 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('when a stage has 100 jobs or more', () => {
+ beforeEach(async () => {
+ pipelineDetailsHandler.mockResolvedValue(mockPipelineResponseWithTooManyJobs);
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('show a warning alert', () => {
+ expect(findJobCountWarning().exists()).toBe(true);
+ expect(findJobCountWarning().props().title).toBe(
+ 'Only the first 100 jobs per stage are displayed',
+ );
+ });
+ });
+
describe('when there is an error', () => {
beforeEach(async () => {
- createComponentWithApollo({
- getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
- });
+ pipelineDetailsHandler.mockRejectedValue(new Error('GraphQL error'));
+ createComponentWithApollo();
await waitForPromises();
});
@@ -270,13 +295,12 @@ describe('Pipeline graph wrapper', () => {
errors: [{ message: 'timeout' }],
};
- const failSucceedFail = jest
- .fn()
+ pipelineDetailsHandler
.mockResolvedValueOnce(errorData)
.mockResolvedValueOnce(mockPipelineResponse)
.mockResolvedValueOnce(errorData);
- createComponentWithApollo({ getPipelineDetailsHandler: failSucceedFail });
+ createComponentWithApollo();
await waitForPromises();
});
@@ -438,9 +462,9 @@ describe('Pipeline graph wrapper', () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
+ pipelineDetailsHandler.mockResolvedValue(nonNeedsResponse);
createComponentWithApollo({
mountFn: mountExtended,
- getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
await waitForPromises();
@@ -460,9 +484,9 @@ describe('Pipeline graph wrapper', () => {
const nonNeedsResponse = { ...mockPipelineResponse };
nonNeedsResponse.data.project.pipeline.usesNeeds = false;
+ pipelineDetailsHandler.mockResolvedValue(nonNeedsResponse);
createComponentWithApollo({
mountFn: mountExtended,
- getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
jest.runOnlyPendingTimers();
diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js
index ec432e98fdf..fca4c43d9fa 100644
--- a/spec/frontend/pipelines/graph/job_name_component_spec.js
+++ b/spec/frontend/pipelines/graph/job_name_component_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import jobNameComponent from '~/pipelines/components/jobs_shared/job_name_component.vue';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('job name component', () => {
let wrapper;
@@ -24,7 +24,7 @@ describe('job name component', () => {
});
it('should render an icon with the provided status', () => {
- expect(wrapper.findComponent(ciIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index bf92cd585d9..8dae2aac664 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -10,7 +10,7 @@ import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/gra
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiStatus from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => {
@@ -87,7 +87,7 @@ describe('Linked pipeline', () => {
});
it('should render an svg within the status container', () => {
- const pipelineStatusElement = wrapper.findComponent(CiStatus);
+ const pipelineStatusElement = wrapper.findComponent(CiIcon);
expect(pipelineStatusElement.find('svg').exists()).toBe(true);
});
@@ -97,7 +97,7 @@ describe('Linked pipeline', () => {
});
it('should have a ci-status child component', () => {
- expect(wrapper.findComponent(CiStatus).exists()).toBe(true);
+ expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
});
it('should render the pipeline id', () => {
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index b012e7f66e1..8d06d6931ed 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,3 +1,4 @@
+import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
import {
BUILD_KIND,
@@ -5,6 +6,14 @@ import {
RETRY_ACTION_TITLE,
} from '~/pipelines/components/graph/constants';
+// We mock this instead of using fixtures for performance reason.
+const mockPipelineResponseCopy = JSON.parse(JSON.stringify(mockPipelineResponse));
+const groups = new Array(100).fill({
+ ...mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes[0],
+});
+mockPipelineResponseCopy.data.project.pipeline.stages.nodes[0].groups.nodes = groups;
+export const mockPipelineResponseWithTooManyJobs = mockPipelineResponseCopy;
+
export const downstream = {
nodes: [
{
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index 50f754393fe..b4ffd2658fe 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -80,7 +80,6 @@ describe('Links Inner component', () => {
};
afterEach(() => {
- jest.restoreAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
deleted file mode 100644
index 18def4ab62c..00000000000
--- a/spec/frontend/pipelines/header_component_spec.js
+++ /dev/null
@@ -1,246 +0,0 @@
-import { GlAlert, GlModal, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import HeaderComponent from '~/pipelines/components/header_component.vue';
-import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
-import {
- mockCancelledPipelineHeader,
- mockFailedPipelineHeader,
- mockFailedPipelineNoPermissions,
- mockRunningPipelineHeader,
- mockRunningPipelineNoPermissions,
- mockSuccessfulPipelineHeader,
-} from './mock_data';
-
-describe('Pipeline details header', () => {
- let wrapper;
- let glModalDirective;
- let mutate = jest.fn();
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findDeleteModal = () => wrapper.findComponent(GlModal);
- const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
- const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]');
- const findDeleteButton = () => wrapper.find('[data-testid="deletePipeline"]');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-
- const defaultProvideOptions = {
- pipelineId: '14',
- pipelineIid: 1,
- paths: {
- pipelinesPath: '/namespace/my-project/-/pipelines',
- fullProject: '/namespace/my-project',
- },
- };
-
- const createComponent = (pipelineMock = mockRunningPipelineHeader, { isLoading } = false) => {
- glModalDirective = jest.fn();
-
- const $apollo = {
- queries: {
- pipeline: {
- loading: isLoading,
- stopPolling: jest.fn(),
- startPolling: jest.fn(),
- },
- },
- mutate,
- };
-
- return shallowMount(HeaderComponent, {
- data() {
- return {
- pipeline: pipelineMock,
- };
- },
- provide: {
- ...defaultProvideOptions,
- },
- directives: {
- glModal: {
- bind(_, { value }) {
- glModalDirective(value);
- },
- },
- },
- mocks: { $apollo },
- });
- };
-
- describe('initial loading', () => {
- beforeEach(() => {
- wrapper = createComponent(null, { isLoading: true });
- });
-
- it('shows a loading state while graphQL is fetching initial data', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
- });
-
- describe('visible state', () => {
- it.each`
- state | pipelineData | retryValue | cancelValue
- ${'cancelled'} | ${mockCancelledPipelineHeader} | ${true} | ${false}
- ${'failed'} | ${mockFailedPipelineHeader} | ${true} | ${false}
- ${'running'} | ${mockRunningPipelineHeader} | ${false} | ${true}
- ${'successful'} | ${mockSuccessfulPipelineHeader} | ${false} | ${false}
- `(
- 'with a $state pipeline, it will show actions: retry $retryValue and cancel $cancelValue',
- ({ pipelineData, retryValue, cancelValue }) => {
- wrapper = createComponent(pipelineData);
-
- expect(findRetryButton().exists()).toBe(retryValue);
- expect(findCancelButton().exists()).toBe(cancelValue);
- },
- );
- });
-
- describe('actions', () => {
- describe('Retry action', () => {
- beforeEach(() => {
- wrapper = createComponent(mockCancelledPipelineHeader);
- });
-
- it('should call retryPipeline Mutation with pipeline id', () => {
- findRetryButton().vm.$emit('click');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: retryPipelineMutation,
- variables: { id: mockCancelledPipelineHeader.id },
- });
- });
-
- it('should render retry action tooltip', () => {
- expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
- });
-
- it('should display error message on failure', async () => {
- const failureMessage = 'failure message';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- pipelineRetry: {
- errors: [failureMessage],
- },
- },
- });
-
- findRetryButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findAlert().text()).toBe(failureMessage);
- });
- });
-
- describe('Retry action failed', () => {
- beforeEach(() => {
- mutate = jest.fn().mockRejectedValue('error');
-
- wrapper = createComponent(mockCancelledPipelineHeader);
- });
-
- it('retry button loading state should reset on error', async () => {
- findRetryButton().vm.$emit('click');
-
- await nextTick();
-
- expect(findRetryButton().props('loading')).toBe(true);
-
- await waitForPromises();
-
- expect(findRetryButton().props('loading')).toBe(false);
- });
- });
-
- describe('Cancel action', () => {
- beforeEach(() => {
- wrapper = createComponent(mockRunningPipelineHeader);
- });
-
- it('should call cancelPipeline Mutation with pipeline id', () => {
- findCancelButton().vm.$emit('click');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: cancelPipelineMutation,
- variables: { id: mockRunningPipelineHeader.id },
- });
- });
-
- it('should render cancel action tooltip', () => {
- expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
- });
-
- it('should display error message on failure', async () => {
- const failureMessage = 'failure message';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- pipelineCancel: {
- errors: [failureMessage],
- },
- },
- });
-
- findCancelButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findAlert().text()).toBe(failureMessage);
- });
- });
-
- describe('Delete action', () => {
- beforeEach(() => {
- wrapper = createComponent(mockFailedPipelineHeader);
- });
-
- it('displays delete modal when clicking on delete and does not call the delete action', () => {
- findDeleteButton().vm.$emit('click');
-
- expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled();
- });
-
- it('should call deletePipeline Mutation with pipeline id when modal is submitted', () => {
- findDeleteModal().vm.$emit('primary');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: deletePipelineMutation,
- variables: { id: mockFailedPipelineHeader.id },
- });
- });
-
- it('should display error message on failure', async () => {
- const failureMessage = 'failure message';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- pipelineDestroy: {
- errors: [failureMessage],
- },
- },
- });
-
- findDeleteModal().vm.$emit('primary');
- await waitForPromises();
-
- expect(findAlert().text()).toBe(failureMessage);
- });
- });
-
- describe('Permissions', () => {
- it('should not display the cancel action if user does not have permission', () => {
- wrapper = createComponent(mockRunningPipelineNoPermissions);
-
- expect(findCancelButton().exists()).toBe(false);
- });
-
- it('should not display the retry action if user does not have permission', () => {
- wrapper = createComponent(mockFailedPipelineNoPermissions);
-
- expect(findRetryButton().exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 62c0d6e2d91..673db3b5178 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -26,19 +26,19 @@ export const pipelineRetryMutationResponseFailed = {
};
export const pipelineCancelMutationResponseSuccess = {
- data: { pipelineRetry: { errors: [] } },
+ data: { pipelineCancel: { errors: [] } },
};
export const pipelineCancelMutationResponseFailed = {
- data: { pipelineRetry: { errors: ['error'] } },
+ data: { pipelineCancel: { errors: ['error'] } },
};
export const pipelineDeleteMutationResponseSuccess = {
- data: { pipelineRetry: { errors: [] } },
+ data: { pipelineDestroy: { errors: [] } },
};
export const pipelineDeleteMutationResponseFailed = {
- data: { pipelineRetry: { errors: ['error'] } },
+ data: { pipelineDestroy: { errors: ['error'] } },
};
export const mockPipelineHeader = {
diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/pipelines/pipeline_details_header_spec.js
index deaf5c6f72f..5c75020afad 100644
--- a/spec/frontend/pipelines/pipeline_details_header_spec.js
+++ b/spec/frontend/pipelines/pipeline_details_header_spec.js
@@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
-import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
@@ -59,19 +58,20 @@ describe('Pipeline details header', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findStatus = () => wrapper.findComponent(CiBadgeLink);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const findAllBadges = () => wrapper.findAllComponents(GlBadge);
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
+ const findCreatedTimeAgo = () => wrapper.findByTestId('pipeline-created-time-ago');
+ const findFinishedTimeAgo = () => wrapper.findByTestId('pipeline-finished-time-ago');
const findPipelineName = () => wrapper.findByTestId('pipeline-name');
const findCommitTitle = () => wrapper.findByTestId('pipeline-commit-title');
const findTotalJobs = () => wrapper.findByTestId('total-jobs');
- const findComputeCredits = () => wrapper.findByTestId('compute-credits');
+ const findComputeMinutes = () => wrapper.findByTestId('compute-minutes');
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text();
const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text();
const findRetryButton = () => wrapper.findByTestId('retry-pipeline');
const findCancelButton = () => wrapper.findByTestId('cancel-pipeline');
const findDeleteButton = () => wrapper.findByTestId('delete-pipeline');
- const findDeleteModal = () => wrapper.findComponent(GlModal);
const findPipelineUserLink = () => wrapper.findByTestId('pipeline-user-link');
const findPipelineDuration = () => wrapper.findByTestId('pipeline-duration-text');
@@ -89,7 +89,7 @@ describe('Pipeline details header', () => {
const defaultProps = {
name: 'Ruby 3.0 master branch pipeline',
totalJobs: '50',
- computeCredits: '0.65',
+ computeMinutes: '0.65',
yamlErrors: 'errors',
failureReason: 'pipeline failed',
badges: {
@@ -216,28 +216,36 @@ describe('Pipeline details header', () => {
});
describe('finished pipeline', () => {
- it('displays compute credits when not zero', async () => {
+ it('displays compute minutes when not zero', async () => {
createComponent();
await waitForPromises();
- expect(findComputeCredits().text()).toBe('0.65');
+ expect(findComputeMinutes().text()).toBe('0.65');
+ });
+
+ it('does not display compute minutes when zero', async () => {
+ createComponent(defaultHandlers, { ...defaultProps, computeMinutes: '0.0' });
+
+ await waitForPromises();
+
+ expect(findComputeMinutes().exists()).toBe(false);
});
- it('does not display compute credits when zero', async () => {
- createComponent(defaultHandlers, { ...defaultProps, computeCredits: '0.0' });
+ it('does not display created time ago', async () => {
+ createComponent();
await waitForPromises();
- expect(findComputeCredits().exists()).toBe(false);
+ expect(findCreatedTimeAgo().exists()).toBe(false);
});
- it('displays time ago', async () => {
+ it('displays finished time ago', async () => {
createComponent();
await waitForPromises();
- expect(findTimeAgo().exists()).toBe(true);
+ expect(findFinishedTimeAgo().exists()).toBe(true);
});
it('displays pipeline duartion text', async () => {
@@ -258,12 +266,12 @@ describe('Pipeline details header', () => {
await waitForPromises();
});
- it('does not display compute credits', () => {
- expect(findComputeCredits().exists()).toBe(false);
+ it('does not display compute minutes', () => {
+ expect(findComputeMinutes().exists()).toBe(false);
});
- it('does not display time ago', () => {
- expect(findTimeAgo().exists()).toBe(false);
+ it('does not display finished time ago', () => {
+ expect(findFinishedTimeAgo().exists()).toBe(false);
});
it('does not display pipeline duration text', () => {
@@ -273,6 +281,10 @@ describe('Pipeline details header', () => {
it('displays pipeline running text', () => {
expect(findPipelineRunningText()).toBe('In progress, queued for 3,600 seconds');
});
+
+ it('displays created time ago', () => {
+ expect(findCreatedTimeAgo().exists()).toBe(true);
+ });
});
describe('running pipeline with duration', () => {
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 9fedbaf9b56..1abc2887682 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,4 +1,9 @@
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
+ GlSprintf,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
@@ -25,25 +30,27 @@ describe('Pipelines Artifacts dropdown', () => {
},
stubs: {
GlSprintf,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
},
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findFirstGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
- const findAllGlDropdownItems = () =>
- wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem);
+ const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findFirstGlDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
it('should render a dropdown with all the provided artifacts', () => {
createComponent();
- expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
+ const [{ items }] = findGlDropdown().props('items');
+ expect(items).toHaveLength(artifacts.length);
});
it('should render a link with the provided path', () => {
createComponent();
- expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
+ expect(findFirstGlDropdownItem().props('item').href).toBe(artifacts[0].path);
expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name);
});
@@ -51,7 +58,7 @@ describe('Pipelines Artifacts dropdown', () => {
it('should not render the dropdown', () => {
createComponent({ mockArtifacts: [] });
- expect(findDropdown().exists()).toBe(false);
+ expect(findGlDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 10752cee841..251d823cc37 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -10,7 +10,6 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
import {
PipelineKeyOptions,
BUTTON_TOOLTIP_RETRY,
@@ -74,7 +73,6 @@ describe('Pipelines Table', () => {
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations);
- const findPipelineFailedJobsWidget = () => wrapper.findComponent(PipelineFailedJobsWidget);
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findStatusTh = () => wrapper.findByTestId('status-th');
@@ -218,30 +216,6 @@ describe('Pipelines Table', () => {
});
});
});
-
- describe('widget', () => {
- describe('when there are no failed jobs', () => {
- beforeEach(() => {
- createComponent(
- { pipelines: [{ ...pipeline, failed_builds: [] }] },
- provideWithDetails,
- );
- });
-
- it('does not renders', () => {
- expect(findPipelineFailedJobsWidget().exists()).toBe(false);
- });
- });
-
- describe('when there are failed jobs', () => {
- beforeEach(() => {
- createComponent({ pipelines: [pipeline] }, provideWithDetails);
- });
- it('renders', () => {
- expect(findPipelineFailedJobsWidget().exists()).toBe(true);
- });
- });
- });
});
describe('tracking', () => {
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index 5afe91c4784..d2aa340a980 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -65,22 +65,11 @@ describe('Timeago component', () => {
expect(time.exists()).toBe(true);
});
- it('should display calendar icon by default', () => {
+ it('should display calendar icon', () => {
createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' });
expect(findCalendarIcon().exists()).toBe(true);
});
-
- it('should hide calendar icon if correct prop is passed', () => {
- createComponent(
- { duration: 0, finished_at: '2017-04-26T12:40:23.277Z' },
- {
- displayCalendarIcon: false,
- },
- );
-
- expect(findCalendarIcon().exists()).toBe(false);
- });
});
describe('without finishedTime', () => {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index fa107600d64..a7052e53062 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,6 +8,7 @@ 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';
+import { setVueErrorHandler, resetVueErrorHandler } from 'helpers/set_vue_error_handler';
jest.mock('~/alert');
@@ -43,7 +44,7 @@ describe('UpdateUsername component', () => {
afterEach(() => {
axiosMock.restore();
- Vue.config.errorHandler = null;
+ resetVueErrorHandler();
});
const findElements = () => {
@@ -60,7 +61,7 @@ describe('UpdateUsername component', () => {
};
const clickModalWithErrorResponse = () => {
- Vue.config.errorHandler = jest.fn(); // silence thrown error
+ setVueErrorHandler({ instance: wrapper.vm, handler: jest.fn() }); // silence thrown error
const { modal } = findElements();
modal.vm.$emit('primary');
return waitForPromises();
diff --git a/spec/frontend/profile/components/follow_spec.js b/spec/frontend/profile/components/follow_spec.js
index 2555e41257f..a2e8d065a46 100644
--- a/spec/frontend/profile/components/follow_spec.js
+++ b/spec/frontend/profile/components/follow_spec.js
@@ -1,11 +1,19 @@
-import { GlAvatarLabeled, GlAvatarLink, GlLoadingIcon, GlPagination } from '@gitlab/ui';
+import {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlEmptyState,
+ GlLoadingIcon,
+ GlPagination,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import users from 'test_fixtures/api/users/followers/get.json';
import Follow from '~/profile/components/follow.vue';
import { DEFAULT_PER_PAGE } from '~/api';
+import { isCurrentUser } from '~/lib/utils/common_utils';
jest.mock('~/rest_api');
+jest.mock('~/lib/utils/common_utils');
describe('FollowersTab', () => {
let wrapper;
@@ -15,6 +23,13 @@ describe('FollowersTab', () => {
loading: false,
page: 1,
totalItems: 50,
+ currentUserEmptyStateTitle: 'UserProfile|You do not have any followers.',
+ visitorEmptyStateTitle: "UserProfile|This user doesn't have any followers.",
+ };
+
+ const defaultProvide = {
+ followEmptyState: '/illustrations/empty-state/empty-friends-md.svg',
+ userId: '1',
};
const createComponent = ({ propsData = {} } = {}) => {
@@ -23,11 +38,13 @@ describe('FollowersTab', () => {
...defaultPropsData,
...propsData,
},
+ provide: defaultProvide,
});
};
const findPagination = () => wrapper.findComponent(GlPagination);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
describe('when `loading` prop is `true`', () => {
it('renders loading icon', () => {
@@ -95,5 +112,35 @@ describe('FollowersTab', () => {
expect(wrapper.emitted('pagination-input')).toEqual([[nextPage]]);
});
});
+
+ describe('when the users prop is empty', () => {
+ describe('when user is the current user', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => true);
+ createComponent({ propsData: { users: [] } });
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultProvide.followEmptyState,
+ title: defaultPropsData.currentUserEmptyStateTitle,
+ });
+ });
+ });
+
+ describe('when user is a visitor', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => false);
+ createComponent({ propsData: { users: [] } });
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultProvide.followEmptyState,
+ title: defaultPropsData.visitorEmptyStateTitle,
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
index 0370005d0a4..75586a2c9ea 100644
--- a/spec/frontend/profile/components/followers_tab_spec.js
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -75,6 +75,8 @@ describe('FollowersTab', () => {
loading: false,
page: 1,
totalItems: 6,
+ currentUserEmptyStateTitle: FollowersTab.i18n.currentUserEmptyStateTitle,
+ visitorEmptyStateTitle: FollowersTab.i18n.visitorEmptyStateTitle,
});
});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
index c0583cf4877..48d84187739 100644
--- a/spec/frontend/profile/components/following_tab_spec.js
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -1,32 +1,114 @@
import { GlBadge, GlTab } from '@gitlab/ui';
-
+import { shallowMount } from '@vue/test-utils';
+import following from 'test_fixtures/api/users/following/get.json';
import { s__ } from '~/locale';
import FollowingTab from '~/profile/components/following_tab.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Follow from '~/profile/components/follow.vue';
+import { getUserFollowing } from '~/rest_api';
+import { createAlert } from '~/alert';
+import waitForPromises from 'helpers/wait_for_promises';
+
+const MOCK_FOLLOWEES_COUNT = 2;
+const MOCK_TOTAL_FOLLOWING = 6;
+const MOCK_PAGE = 1;
+
+jest.mock('~/rest_api');
+jest.mock('~/alert');
describe('FollowingTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowingTab, {
+ wrapper = shallowMount(FollowingTab, {
provide: {
- followeesCount: 3,
+ followeesCount: MOCK_FOLLOWEES_COUNT,
+ userId: 1,
+ },
+ stubs: {
+ GlTab,
},
});
};
- it('renders `GlTab` and sets title', () => {
- createComponent();
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findFollow = () => wrapper.findComponent(Follow);
+
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ getUserFollowing.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ });
+
+ it('renders `Follow` component and sets `loading` prop to `true`', () => {
+ expect(findFollow().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(() => {
+ getUserFollowing.mockResolvedValueOnce({
+ data: following,
+ headers: { 'X-TOTAL': `${MOCK_TOTAL_FOLLOWING}` },
+ });
+ createComponent();
+ });
+
+ it('renders `GlTab` and sets title', () => {
+ expect(wrapper.findComponent(GlTab).text()).toContain(s__('UserProfile|Following'));
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ expect(findGlBadge().props('size')).toBe('sm');
+ expect(findGlBadge().text()).toBe(`${MOCK_FOLLOWEES_COUNT}`);
+ });
+
+ it('renders `Follow` component and passes correct props', () => {
+ expect(findFollow().props()).toMatchObject({
+ users: following,
+ loading: false,
+ page: MOCK_PAGE,
+ totalItems: MOCK_TOTAL_FOLLOWING,
+ currentUserEmptyStateTitle: FollowingTab.i18n.currentUserEmptyStateTitle,
+ visitorEmptyStateTitle: FollowingTab.i18n.visitorEmptyStateTitle,
+ });
+ });
+
+ describe('when `Follow` component emits `pagination-input` event', () => {
+ it('calls API and updates `users` and `page` props', async () => {
+ const NEXT_PAGE = MOCK_PAGE + 1;
+ const NEXT_PAGE_FOLLOWING = [{ id: 999, name: 'page 2 following' }];
- expect(wrapper.findComponent(GlTab).element.textContent).toContain(
- s__('UserProfile|Following'),
- );
+ getUserFollowing.mockResolvedValueOnce({
+ data: NEXT_PAGE_FOLLOWING,
+ headers: { 'X-TOTAL': `${MOCK_TOTAL_FOLLOWING}` },
+ });
+
+ findFollow().vm.$emit('pagination-input', NEXT_PAGE);
+
+ await waitForPromises();
+
+ expect(findFollow().props()).toMatchObject({
+ users: NEXT_PAGE_FOLLOWING,
+ loading: false,
+ page: NEXT_PAGE,
+ totalItems: MOCK_TOTAL_FOLLOWING,
+ });
+ });
+ });
});
- it('renders `GlBadge`, sets size and content', () => {
- createComponent();
+ describe('when API request is not successful', () => {
+ beforeEach(() => {
+ getUserFollowing.mockRejectedValueOnce(new Error());
+ createComponent();
+ });
- expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
- expect(wrapper.findComponent(GlBadge).element.textContent).toBe('3');
+ it('shows error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FollowingTab.i18n.errorMessage,
+ error: new Error(),
+ captureError: true,
+ });
+ });
});
});
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
index f3dda2e205f..3474bbf8d0c 100644
--- a/spec/frontend/profile/components/profile_tabs_spec.js
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -4,6 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/alert';
import { getUserProjects } from '~/rest_api';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants';
import OverviewTab from '~/profile/components/overview_tab.vue';
import ActivityTab from '~/profile/components/activity_tab.vue';
import GroupsTab from '~/profile/components/groups_tab.vue';
@@ -60,18 +61,30 @@ describe('ProfileTabs', () => {
});
describe('when personal projects API request is successful', () => {
- beforeEach(async () => {
+ it('passes correct props to `OverviewTab` component', async () => {
getUserProjects.mockResolvedValueOnce({ data: projects });
createComponent();
await waitForPromises();
- });
- it('passes correct props to `OverviewTab` component', () => {
expect(wrapper.findComponent(OverviewTab).props()).toMatchObject({
personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }),
personalProjectsLoading: false,
});
});
+
+ describe('when projects do not have `visibility` key', () => {
+ it('sets visibility to public', async () => {
+ const [{ visibility, ...projectWithoutVisibility }] = projects;
+
+ getUserProjects.mockResolvedValueOnce({ data: [projectWithoutVisibility] });
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(OverviewTab).props('personalProjects')[0].visibility).toBe(
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+ );
+ });
+ });
});
describe('when personal projects API request is not successful', () => {
diff --git a/spec/frontend/profile/components/snippets/snippets_tab_spec.js b/spec/frontend/profile/components/snippets/snippets_tab_spec.js
index 47e2fbcf2c0..5992bb03e4d 100644
--- a/spec/frontend/profile/components/snippets/snippets_tab_spec.js
+++ b/spec/frontend/profile/components/snippets/snippets_tab_spec.js
@@ -7,6 +7,7 @@ import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants';
import SnippetsTab from '~/profile/components/snippets/snippets_tab.vue';
import SnippetRow from '~/profile/components/snippets/snippet_row.vue';
import getUserSnippets from '~/profile/components/graphql/get_user_snippets.query.graphql';
+import { isCurrentUser } from '~/lib/utils/common_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
@@ -15,8 +16,14 @@ import {
MOCK_USER_SNIPPETS_RES,
MOCK_USER_SNIPPETS_PAGINATION_RES,
MOCK_USER_SNIPPETS_EMPTY_RES,
+ MOCK_NEW_SNIPPET_PATH,
} from 'jest/profile/mock_data';
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/helpers/help_page_helper', () => ({
+ helpPagePath: jest.fn().mockImplementation(() => 'http://127.0.0.1:3000/help/user/snippets'),
+}));
+
Vue.use(VueApollo);
describe('UserProfileSnippetsTab', () => {
@@ -32,6 +39,7 @@ describe('UserProfileSnippetsTab', () => {
provide: {
userId: MOCK_USER.id,
snippetsEmptyState: MOCK_SNIPPETS_EMPTY_STATE,
+ newSnippetPath: MOCK_NEW_SNIPPET_PATH,
},
});
};
@@ -52,9 +60,38 @@ describe('UserProfileSnippetsTab', () => {
expect(findSnippetRows().exists()).toBe(false);
});
- it('does render empty state with correct svg', () => {
- expect(findGlEmptyState().exists()).toBe(true);
- expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_SNIPPETS_EMPTY_STATE);
+ describe('when user is the current user', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => true);
+ createComponent();
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ svgPath: MOCK_SNIPPETS_EMPTY_STATE,
+ title: SnippetsTab.i18n.currentUserEmptyStateTitle,
+ description: SnippetsTab.i18n.emptyStateDescription,
+ primaryButtonLink: MOCK_NEW_SNIPPET_PATH,
+ primaryButtonText: SnippetsTab.i18n.newSnippet,
+ secondaryButtonLink: 'http://127.0.0.1:3000/help/user/snippets',
+ secondaryButtonText: SnippetsTab.i18n.learnMore,
+ });
+ });
+ });
+
+ describe('when user is a visitor', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => false);
+ createComponent();
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ svgPath: MOCK_SNIPPETS_EMPTY_STATE,
+ title: SnippetsTab.i18n.visitorEmptyStateTitle,
+ description: null,
+ });
+ });
});
});
diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js
index 856534aebd3..6c4ff0a84f9 100644
--- a/spec/frontend/profile/mock_data.js
+++ b/spec/frontend/profile/mock_data.js
@@ -22,6 +22,7 @@ export const userCalendarResponse = {
};
export const MOCK_SNIPPETS_EMPTY_STATE = 'illustrations/empty-state/empty-snippets-md.svg';
+export const MOCK_NEW_SNIPPET_PATH = '/-/snippets/new';
export const MOCK_USER = {
id: '1',
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 21167dccda9..144d9e76869 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -47,10 +47,6 @@ describe('ProfilePreferences component', () => {
);
}
- function findIntegrationsDivider() {
- return wrapper.findByTestId('profile-preferences-integrations-rule');
- }
-
function findIntegrationsHeading() {
return wrapper.findByTestId('profile-preferences-integrations-heading');
}
@@ -86,21 +82,17 @@ describe('ProfilePreferences component', () => {
it('should not render Integrations section', () => {
wrapper = createComponent();
const views = wrapper.findAllComponents(IntegrationView);
- const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
- expect(divider.exists()).toBe(false);
expect(heading.exists()).toBe(false);
expect(views).toHaveLength(0);
});
it('should render Integration section', () => {
wrapper = createComponent({ provide: { integrationViews } });
- const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
const views = wrapper.findAllComponents(IntegrationView);
- expect(divider.exists()).toBe(true);
expect(heading.exists()).toBe(true);
expect(views).toHaveLength(integrationViews.length);
});
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 630b8feafbc..50e3f2d0f37 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -1,12 +1,17 @@
-import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
-import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
Vue.use(Vuex);
@@ -44,6 +49,10 @@ describe('Author Select', () => {
propsData: {
projectCommitsEl: document.querySelector('.js-project-commits-show'),
},
+ stubs: {
+ GlCollapsibleListbox,
+ GlListboxItem,
+ },
});
};
@@ -58,11 +67,9 @@ describe('Author Select', () => {
resetHTMLFixture();
});
- const findDropdownContainer = () => wrapper.findComponent({ ref: 'dropdownContainer' });
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findListboxContainer = () => wrapper.findComponent({ ref: 'listboxContainer' });
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
describe('user is searching via "filter by commit message"', () => {
beforeEach(() => {
@@ -70,24 +77,28 @@ describe('Author Select', () => {
createComponent();
});
- it('does not disable dropdown container', () => {
- expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
+ it('does not disable listbox container', () => {
+ expect(findListboxContainer().attributes('disabled')).toBeUndefined();
});
it('has correct tooltip message', () => {
- expect(findDropdownContainer().attributes('title')).toBe(
+ expect(findListboxContainer().attributes('title')).toBe(
'Searching by both author and message is currently not supported.',
);
});
- it('disables dropdown', () => {
- expect(findDropdown().attributes('disabled')).toBeDefined();
+ it('disables listbox', () => {
+ expect(findListbox().attributes('disabled')).toBeDefined();
});
});
- describe('dropdown', () => {
+ describe('listbox', () => {
+ beforeEach(() => {
+ store.state.commitsPath = commitsPath;
+ });
+
it('displays correct default text', () => {
- expect(findDropdown().attributes('text')).toBe('Author');
+ expect(findListbox().props('toggleText')).toBe('Author');
});
it('displays the current selected author', async () => {
@@ -95,81 +106,62 @@ describe('Author Select', () => {
createComponent();
await nextTick();
- expect(findDropdown().attributes('text')).toBe(currentAuthor);
+ expect(findListbox().props('toggleText')).toBe(currentAuthor);
});
it('displays correct header text', () => {
- expect(findDropdownHeader().text()).toBe('Search by author');
+ expect(findListbox().props('headerText')).toBe('Search by author');
});
it('does not have popover text by default', () => {
expect(wrapper.attributes('title')).toBeUndefined();
});
+
+ it('passes selected author to redirectPath', () => {
+ const redirectPath = `${commitsPath}?author=${currentAuthor}`;
+
+ findListbox().vm.$emit('select', currentAuthor);
+
+ expect(visitUrl).toHaveBeenCalledWith(redirectPath);
+ });
+
+ it('does not pass any author to redirectPath', () => {
+ const redirectPath = commitsPath;
+
+ findListbox().vm.$emit('select', '');
+
+ expect(visitUrl).toHaveBeenCalledWith(redirectPath);
+ });
});
- describe('dropdown search box', () => {
+ describe('listbox search box', () => {
it('has correct placeholder', () => {
- expect(findSearchBox().attributes('placeholder')).toBe('Search');
+ expect(findListbox().props('searchPlaceholder')).toBe('Search');
});
it('fetch authors on input change', () => {
const authorName = 'lorem';
- findSearchBox().vm.$emit('input', authorName);
+ findListbox().vm.$emit('search', authorName);
expect(store.actions.fetchAuthors).toHaveBeenCalledWith(expect.anything(), authorName);
});
});
- describe('dropdown list', () => {
+ describe('listbox list', () => {
beforeEach(() => {
store.state.commitsAuthors = authors;
- store.state.commitsPath = commitsPath;
});
it('has a "Any Author" as the first list item', () => {
- expect(findDropdownItems().at(0).text()).toBe('Any Author');
+ expect(findListboxItems().at(0).text()).toBe('Any Author');
});
it('displays the project authors', () => {
- expect(findDropdownItems()).toHaveLength(authors.length + 1);
- });
-
- it('has the correct props', async () => {
- setWindowLocation(`?author=${currentAuthor}`);
- createComponent();
-
- const [{ avatar_url: avatarUrl, username }] = authors;
- const result = {
- avatarUrl,
- secondaryText: username,
- isChecked: true,
- };
-
- await nextTick();
- expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
+ expect(findListboxItems()).toHaveLength(authors.length + 1);
});
it("display the author's name", () => {
- expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
- });
-
- it('passes selected author to redirectPath', () => {
- const redirectToUrl = `${commitsPath}?author=${currentAuthor}`;
- const spy = jest.spyOn(urlUtility, 'redirectTo');
- spy.mockImplementation(() => 'mock');
-
- findDropdownItems().at(1).vm.$emit('click');
-
- expect(spy).toHaveBeenCalledWith(redirectToUrl);
- });
-
- it('does not pass any author to redirectPath', () => {
- const redirectToUrl = commitsPath;
- const spy = jest.spyOn(urlUtility, 'redirectTo');
- spy.mockImplementation();
-
- findDropdownItems().at(0).vm.$emit('click');
- expect(spy).toHaveBeenCalledWith(redirectToUrl);
+ expect(findListboxItems().at(1).text()).toContain(currentAuthor);
});
});
});
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index ee96f46ea0c..6cc76d4a573 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -1,23 +1,37 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink, GlSprintf, GlFormGroup, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CompareApp from '~/projects/compare/components/app.vue';
+import {
+ COMPARE_REVISIONS_DOCS_URL,
+ I18N,
+ COMPARE_OPTIONS,
+ COMPARE_OPTIONS_INPUT_NAME,
+} from '~/projects/compare/constants';
import RevisionCard from '~/projects/compare/components/revision_card.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { appDefaultProps as defaultProps } from './mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('CompareApp component', () => {
let wrapper;
- const findSourceRevisionCard = () => wrapper.find('[data-testid="sourceRevisionCard"]');
- const findTargetRevisionCard = () => wrapper.find('[data-testid="targetRevisionCard"]');
+ const findSourceRevisionCard = () => wrapper.findByTestId('sourceRevisionCard');
+ const findTargetRevisionCard = () => wrapper.findByTestId('targetRevisionCard');
const createComponent = (props = {}) => {
- wrapper = shallowMount(CompareApp, {
+ wrapper = shallowMountExtended(CompareApp, {
propsData: {
...defaultProps,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ stubs: {
+ GlSprintf,
+ GlFormRadioGroup,
+ },
});
};
@@ -37,6 +51,21 @@ describe('CompareApp component', () => {
);
});
+ it('renders title', () => {
+ const title = wrapper.find('h1');
+ expect(title.text()).toBe(I18N.title);
+ });
+
+ it('renders subtitle', () => {
+ const subtitle = wrapper.find('p');
+ expect(subtitle.text()).toMatchInterpolatedText(I18N.subtitle);
+ });
+
+ it('renders link to docs', () => {
+ const docsLink = wrapper.findComponent(GlLink);
+ expect(docsLink.attributes('href')).toBe(COMPARE_REVISIONS_DOCS_URL);
+ });
+
it('contains the correct form attributes', () => {
expect(wrapper.attributes('action')).toBe(defaultProps.projectCompareIndexPath);
expect(wrapper.attributes('method')).toBe('POST');
@@ -48,20 +77,16 @@ describe('CompareApp component', () => {
);
});
- it('has ellipsis', () => {
- expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true);
- });
-
it('render Source and Target BranchDropdown components', () => {
const revisionCards = wrapper.findAllComponents(RevisionCard);
expect(revisionCards.length).toBe(2);
- expect(revisionCards.at(0).props('revisionText')).toBe('Source');
- expect(revisionCards.at(1).props('revisionText')).toBe('Target');
+ expect(revisionCards.at(0).props('revisionText')).toBe(I18N.source);
+ expect(revisionCards.at(1).props('revisionText')).toBe(I18N.target);
});
describe('compare button', () => {
- const findCompareButton = () => wrapper.findComponent(GlButton);
+ const findCompareButton = () => wrapper.findByTestId('compare-button');
it('renders button', () => {
expect(findCompareButton().exists()).toBe(true);
@@ -109,14 +134,19 @@ describe('CompareApp component', () => {
});
describe('swap revisions button', () => {
- const findSwapRevisionsButton = () => wrapper.find('[data-testid="swapRevisionsButton"]');
+ const findSwapRevisionsButton = () => wrapper.findByTestId('swapRevisionsButton');
it('renders the swap revisions button', () => {
expect(findSwapRevisionsButton().exists()).toBe(true);
});
- it('has the correct text', () => {
- expect(findSwapRevisionsButton().text()).toBe('Swap revisions');
+ it('renders icon', () => {
+ expect(findSwapRevisionsButton().findComponent(GlIcon).props('name')).toBe('substitute');
+ });
+
+ it('has tooltip', () => {
+ const tooltip = getBinding(findSwapRevisionsButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(I18N.swapRevisions);
});
it('swaps revisions when clicked', async () => {
@@ -129,43 +159,43 @@ describe('CompareApp component', () => {
});
});
- describe('mode dropdown', () => {
- const findModeDropdownButton = () => wrapper.find('[data-testid="modeDropdown"]');
- const findEnableStraightModeButton = () =>
- wrapper.find('[data-testid="enableStraightModeButton"]');
- const findDisableStraightModeButton = () =>
- wrapper.find('[data-testid="disableStraightModeButton"]');
+ describe('compare options', () => {
+ const findGroup = () => wrapper.findComponent(GlFormGroup);
+ const findOptionsGroup = () => wrapper.findComponent(GlFormRadioGroup);
- it('renders the mode dropdown button', () => {
- expect(findModeDropdownButton().exists()).toBe(true);
- });
+ const findOptions = () => wrapper.findAllComponents(GlFormRadio);
- it('has the correct text', () => {
- expect(findEnableStraightModeButton().text()).toBe('...');
- expect(findDisableStraightModeButton().text()).toBe('..');
+ it('renders label for the compare options', () => {
+ expect(findGroup().attributes('label')).toBe(I18N.optionsLabel);
});
- it('straight mode button when clicked', async () => {
- expect(wrapper.props('straight')).toBe(false);
- expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
+ it('correct input name', () => {
+ expect(findOptionsGroup().attributes('name')).toBe(COMPARE_OPTIONS_INPUT_NAME);
+ });
- findEnableStraightModeButton().vm.$emit('click');
+ it('renders "only incoming changes" option', () => {
+ expect(findOptions().at(0).text()).toBe(COMPARE_OPTIONS[0].text);
+ });
- await nextTick();
+ it('renders "since source was created" option', () => {
+ expect(findOptions().at(1).text()).toBe(COMPARE_OPTIONS[1].text);
+ });
- expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('true');
+ it('straight mode button when clicked', async () => {
+ expect(wrapper.props('straight')).toBe(false);
+ expect(wrapper.vm.isStraight).toBe(false);
- findDisableStraightModeButton().vm.$emit('click');
+ findOptionsGroup().vm.$emit('input', COMPARE_OPTIONS[1].value);
await nextTick();
- expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
+ expect(wrapper.vm.isStraight).toBe(true);
});
});
describe('merge request buttons', () => {
- const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
- const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
+ const findProjectMrButton = () => wrapper.findByTestId('projectMrButton');
+ const findCreateMrButton = () => wrapper.findByTestId('createMrButton');
it('does not have merge request buttons', () => {
createComponent();
diff --git a/spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap b/spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap
new file mode 100644
index 00000000000..0c4d63c3509
--- /dev/null
+++ b/spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Experimental new project creation app creates correct panels 1`] = `
+Array [
+ Object {
+ "description": "Create a blank project to store your files, plan your work, and collaborate on code, among other things.",
+ "imageSrc": "file-mock",
+ "key": "blank",
+ "name": "blank_project",
+ "selector": "#blank-project-pane",
+ "title": "Create blank project",
+ },
+ Object {
+ "description": "Create a project pre-populated with the necessary files to get you started quickly.",
+ "imageSrc": "file-mock",
+ "key": "template",
+ "name": "create_from_template",
+ "selector": "#create-from-template-pane",
+ "title": "Create from template",
+ },
+ Object {
+ "description": "Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.",
+ "imageSrc": "file-mock",
+ "key": "import",
+ "name": "import_project",
+ "selector": "#import-project-pane",
+ "title": "Import project",
+ },
+]
+`;
diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index 60d8385eb91..006114e7254 100644
--- a/spec/frontend/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -23,6 +23,12 @@ describe('Experimental new project creation app', () => {
expect(wrapper.find(guidelineSelector).text()).toBe(DEMO_GUIDELINES);
});
+ it('creates correct panels', () => {
+ createComponent();
+
+ expect(findNewNamespacePage().props('panels')).toMatchSnapshot();
+ });
+
it.each`
isCiCdAvailable | outcome
${false} | ${'do not show CI/CD panel'}
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
index d51360a7597..a94d7669b2b 100644
--- a/spec/frontend/projects/settings/access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/access_dropdown_spec.js
@@ -158,6 +158,31 @@ describe('AccessDropdown', () => {
expect(template).not.toContain(user.name);
});
+
+ it('show user avatar correctly', () => {
+ const user = {
+ id: 613,
+ avatar_url: 'some_valid_avatar.png',
+ name: 'test',
+ username: 'test',
+ };
+ const template = dropdown.userRowHtml(user);
+
+ expect(template).toContain(user.avatar_url);
+ expect(template).not.toContain('identicon');
+ });
+
+ it('show identicon when user do not have avatar', () => {
+ const user = {
+ id: 613,
+ avatar_url: '',
+ name: 'test',
+ username: 'test',
+ };
+ const template = dropdown.userRowHtml(user);
+
+ expect(template).toContain('identicon');
+ });
});
describe('deployKeyRowHtml', () => {
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 077995ab6e4..76d45692a63 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
@@ -91,7 +91,6 @@ describe('View branch rules', () => {
expect(findBranchName().text()).toBe(I18N.allBranches);
expect(findBranchTitle().text()).toBe(I18N.targetBranch);
- jest.restoreAllMocks();
});
it('renders the correct branch title', () => {
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 7f6ecbac748..b84d1c9c0aa 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
@@ -13,8 +13,8 @@ describe('ServiceDeskRoot', () => {
let spy;
const provideData = {
- customEmail: 'custom.email@example.com',
- customEmailEnabled: true,
+ serviceDeskEmail: 'custom.email@example.com',
+ serviceDeskEmailEnabled: true,
endpoint: '/gitlab-org/gitlab-test/service_desk',
initialIncomingEmail: 'servicedeskaddress@example.com',
initialIsEnabled: true,
@@ -52,8 +52,8 @@ describe('ServiceDeskRoot', () => {
wrapper = createComponent();
expect(wrapper.findComponent(ServiceDeskSetting).props()).toEqual({
- customEmail: provideData.customEmail,
- customEmailEnabled: provideData.customEmailEnabled,
+ serviceDeskEmail: provideData.serviceDeskEmail,
+ serviceDeskEmailEnabled: provideData.serviceDeskEmailEnabled,
incomingEmail: provideData.initialIncomingEmail,
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
@@ -80,7 +80,7 @@ describe('ServiceDeskRoot', () => {
const alertBodyLink = alertEl.findComponent(GlLink);
expect(alertBodyLink.exists()).toBe(true);
expect(alertBodyLink.attributes('href')).toBe(
- '/help/user/project/service_desk.html#use-a-custom-email-address',
+ '/help/user/project/service_desk.html#use-an-additional-service-desk-alias-email',
);
expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 5631927cc2f..260fd200f03 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -134,26 +134,26 @@ describe('ServiceDeskSetting', () => {
});
});
- describe('with customEmail', () => {
- describe('customEmail is different than incomingEmail', () => {
+ describe('with serviceDeskEmail', () => {
+ describe('serviceDeskEmail is different than incomingEmail', () => {
const incomingEmail = 'foo@bar.com';
- const customEmail = 'custom@bar.com';
+ const serviceDeskEmail = 'servicedesk@bar.com';
beforeEach(() => {
wrapper = createComponent({
- props: { incomingEmail, customEmail },
+ props: { incomingEmail, serviceDeskEmail },
});
});
- it('should see custom email', () => {
- expect(findIncomingEmail().element.value).toEqual(customEmail);
+ it('should see service desk email', () => {
+ expect(findIncomingEmail().element.value).toEqual(serviceDeskEmail);
});
});
describe('project suffix', () => {
it('input is hidden', () => {
wrapper = createComponent({
- props: { customEmailEnabled: false },
+ props: { serviceDeskEmailEnabled: false },
});
const input = wrapper.findByTestId('project-suffix');
@@ -163,7 +163,7 @@ describe('ServiceDeskSetting', () => {
it('input is enabled', () => {
wrapper = createComponent({
- props: { customEmailEnabled: true },
+ props: { serviceDeskEmailEnabled: true },
});
const input = wrapper.findByTestId('project-suffix');
@@ -174,7 +174,7 @@ describe('ServiceDeskSetting', () => {
it('shows error when value contains uppercase or special chars', async () => {
wrapper = createComponent({
- props: { email: 'foo@bar.com', customEmailEnabled: true },
+ props: { email: 'foo@bar.com', serviceDeskEmailEnabled: true },
});
const input = wrapper.findByTestId('project-suffix');
@@ -189,16 +189,16 @@ describe('ServiceDeskSetting', () => {
});
});
- describe('customEmail is the same as incomingEmail', () => {
+ describe('serviceDeskEmail is the same as incomingEmail', () => {
const email = 'foo@bar.com';
beforeEach(() => {
wrapper = createComponent({
- props: { incomingEmail: email, customEmail: email },
+ props: { incomingEmail: email, serviceDeskEmail: email },
});
});
- it('should see custom email', () => {
+ it('should see service desk email', () => {
expect(findIncomingEmail().element.value).toEqual(email);
});
});
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index 1d0faebbcb2..89f4694d1f8 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -60,14 +60,15 @@ describe('TerraformNotificationBanner', () => {
describe('when close button is clicked', () => {
beforeEach(() => {
- wrapper.vm.$refs.calloutDismisser.dismiss = userCalloutDismissSpy;
findBanner().vm.$emit('close');
});
+
it('should send the dismiss event', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, DISMISS_EVENT, {
label: EVENT_LABEL,
});
});
+
it('should call the dismiss callback', () => {
expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
});
diff --git a/spec/frontend/related_issues/components/related_issuable_input_spec.js b/spec/frontend/related_issues/components/related_issuable_input_spec.js
index f7333bf6893..30f957a4c45 100644
--- a/spec/frontend/related_issues/components/related_issuable_input_spec.js
+++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js
@@ -1,38 +1,37 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
+import GfmAutoComplete from '~/gfm_auto_complete';
import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { PathIdSeparator } from '~/related_issues/constants';
-jest.mock('ee_else_ce/gfm_auto_complete', () => {
- return function gfmAutoComplete() {
- return {
- constructor() {},
- setup() {},
- };
- };
-});
+jest.mock('~/gfm_auto_complete');
describe('RelatedIssuableInput', () => {
- let propsData;
-
- beforeEach(() => {
- propsData = {
- inputValue: '',
- references: [],
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: TYPE_ISSUE,
- autoCompleteSources: {
- issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
+ let wrapper;
+
+ const autoCompleteSources = {
+ issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
+ };
+
+ const mountComponent = (props = {}) => {
+ wrapper = shallowMount(RelatedIssuableInput, {
+ propsData: {
+ inputValue: '',
+ references: [],
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: TYPE_ISSUE,
+ autoCompleteSources,
+ ...props,
},
- };
- });
+ attachTo: document.body,
+ });
+ };
describe('autocomplete', () => {
describe('with autoCompleteSources', () => {
it('shows placeholder text', () => {
- const wrapper = shallowMount(RelatedIssuableInput, { propsData });
+ mountComponent();
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
'Paste issue link or <#issue id>',
@@ -40,51 +39,32 @@ describe('RelatedIssuableInput', () => {
});
it('has GfmAutoComplete', () => {
- const wrapper = shallowMount(RelatedIssuableInput, { propsData });
+ mountComponent();
- expect(wrapper.vm.gfmAutoComplete).toBeDefined();
+ expect(GfmAutoComplete).toHaveBeenCalledWith(autoCompleteSources);
});
});
describe('with no autoCompleteSources', () => {
it('shows placeholder text', () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData: {
- ...propsData,
- references: ['!1', '!2'],
- },
- });
+ mountComponent({ references: ['!1', '!2'] });
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
});
it('does not have GfmAutoComplete', () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData: {
- ...propsData,
- autoCompleteSources: {},
- },
- });
+ mountComponent({ autoCompleteSources: {} });
- expect(wrapper.vm.gfmAutoComplete).not.toBeDefined();
+ expect(GfmAutoComplete).not.toHaveBeenCalled();
});
});
});
describe('focus', () => {
it('when clicking anywhere on the input wrapper it should focus the input', async () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData: {
- ...propsData,
- references: ['foo', 'bar'],
- },
- // We need to attach to document, so that `document.activeElement` is properly set in jsdom
- attachTo: document.body,
- });
-
- wrapper.find('li').trigger('click');
+ mountComponent({ references: ['foo', 'bar'] });
- await nextTick();
+ await wrapper.find('li').trigger('click');
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
});
@@ -92,11 +72,7 @@ describe('RelatedIssuableInput', () => {
describe('when filling in the input', () => {
it('emits addIssuableFormInput with data', () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData,
- });
-
- wrapper.vm.$emit = jest.fn();
+ mountComponent();
const newInputValue = 'filling in things';
const untouchedRawReferences = newInputValue.trim().split(/\s/);
@@ -108,12 +84,16 @@ describe('RelatedIssuableInput', () => {
input.element.selectionEnd = newInputValue.length;
input.trigger('input');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', {
- newValue: newInputValue,
- caretPos: newInputValue.length,
- untouchedRawReferences,
- touchedReference,
- });
+ expect(wrapper.emitted('addIssuableFormInput')).toEqual([
+ [
+ {
+ newValue: newInputValue,
+ caretPos: newInputValue.length,
+ untouchedRawReferences,
+ touchedReference,
+ },
+ ],
+ ]);
});
});
});
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index 923d84ae2b3..b241eb9acd4 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -60,9 +60,22 @@ describe('releases_pagination.vue', () => {
const findPrevButton = () => wrapper.findByTestId('prevButton');
const findNextButton = () => wrapper.findByTestId('nextButton');
+ describe('when there is only one page of results', () => {
+ beforeEach(() => {
+ createComponent(singlePageInfo);
+ });
+
+ it('hides the "Prev" button', () => {
+ expect(findPrevButton().exists()).toBe(false);
+ });
+
+ it('hides the "Next" button', () => {
+ expect(findNextButton().exists()).toBe(false);
+ });
+ });
+
describe.each`
description | pageInfo | prevEnabled | nextEnabled
- ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false}
${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true}
${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false}
${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true}
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 6825d4afecf..ede04390586 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -12,13 +12,15 @@ exports[`Repository last commit component renders commit widget 1`] = `
imgsize="32"
imgsrc="https://test.com"
linkhref="/test"
+ popoveruserid=""
+ popoverusername=""
tooltipplacement="top"
tooltiptext=""
username=""
/>
<div
- class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-flex-grow-1 gl-min-w-0"
+ class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
>
<div
class="commit-content"
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index ecd617ca44b..e2bb7cdb2d7 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -11,7 +11,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
-import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
@@ -52,6 +52,8 @@ let userInfoMockResolver;
let projectInfoMockResolver;
let applicationInfoMockResolver;
+Vue.use(Vuex);
+
const mockAxios = new MockAdapter(axios);
const createMockStore = () =>
@@ -150,6 +152,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
...inject,
glFeatures: {
highlightJs,
+ highlightJsWorker: false,
},
},
}),
@@ -403,7 +406,7 @@ describe('Blob content viewer component', () => {
await waitForPromises();
- expect(loadViewer).toHaveBeenCalledWith(viewer, false);
+ expect(loadViewer).toHaveBeenCalledWith(viewer, false, false, 'javascript');
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index f4baa817d32..46a7f2ee1bb 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@@ -11,6 +11,7 @@ import permissionsQuery from 'shared_queries/repository/permissions.query.graphq
import projectPathQuery from '~/repository/queries/project_path.query.graphql';
import createApolloProvider from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
const defaultMockRoute = {
name: 'blobPath',
@@ -61,6 +62,7 @@ describe('Repository breadcrumbs component', () => {
},
stubs: {
RouterLink: RouterLinkStub,
+ GlDisclosureDropdown,
},
mocks: {
$route: {
@@ -71,7 +73,8 @@ describe('Repository breadcrumbs component', () => {
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub);
@@ -146,7 +149,11 @@ describe('Repository breadcrumbs component', () => {
`(
'does render add to tree dropdown $isRendered when route is $routeName',
({ routeName, isRendered }) => {
- factory('app/assets/javascripts.js', { canCollaborate: true }, { name: routeName });
+ factory(
+ 'app/assets/javascripts.js',
+ { canCollaborate: true, canEditTree: true },
+ { name: routeName },
+ );
expect(findDropdown().exists()).toBe(isRendered);
},
);
@@ -156,7 +163,7 @@ describe('Repository breadcrumbs component', () => {
createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }),
);
- factory('/', { canCollaborate: true });
+ factory('/', { canCollaborate: true, canEditTree: true });
await nextTick();
expect(findDropdown().exists()).toBe(true);
@@ -193,4 +200,32 @@ describe('Repository breadcrumbs component', () => {
expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir');
});
});
+
+ describe('"this repository" dropdown group', () => {
+ it('renders when user has pushCode permissions', async () => {
+ permissionsQuerySpy.mockResolvedValue(
+ createPermissionsQueryResponse({
+ pushCode: true,
+ }),
+ );
+
+ factory('/', { canCollaborate: true });
+ await waitForPromises();
+
+ expect(findDropdownGroup().props('group').name).toBe(__('This repository'));
+ });
+
+ it('does not render when user does not have pushCode permissions', async () => {
+ permissionsQuerySpy.mockResolvedValue(
+ createPermissionsQueryResponse({
+ pushCode: false,
+ }),
+ );
+
+ factory('/', { canCollaborate: true });
+ await waitForPromises();
+
+ expect(findDropdownGroup().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
index 5f872749581..fd14f01747a 100644
--- a/spec/frontend/repository/mixins/highlight_mixin_spec.js
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -31,10 +31,13 @@ describe('HighlightMixin', () => {
const dummyComponent = {
mixins: [highlightMixin],
- inject: { highlightWorker: { default: workerMock } },
+ inject: {
+ highlightWorker: { default: workerMock },
+ glFeatures: { default: { highlightJsWorker: true } },
+ },
template: '<div>{{chunks[0]?.highlightedContent}}</div>',
created() {
- this.initHighlightWorker({ rawTextBlob, simpleViewer, language });
+ this.initHighlightWorker({ rawTextBlob, simpleViewer, language, fileType });
},
methods: { onError: onErrorMock },
};
@@ -84,6 +87,7 @@ describe('HighlightMixin', () => {
expect(workerMock.postMessage.mock.calls[1][0]).toMatchObject({
content: rawTextBlob,
language: languageMock,
+ fileType: TEXT_FILE_TYPE,
});
});
});
diff --git a/spec/frontend/scripts/frontend/po_to_json_spec.js b/spec/frontend/scripts/frontend/po_to_json_spec.js
index 858e3c9d3c7..47d5ccfefd4 100644
--- a/spec/frontend/scripts/frontend/po_to_json_spec.js
+++ b/spec/frontend/scripts/frontend/po_to_json_spec.js
@@ -168,7 +168,7 @@ msgstr ""
});
describe('escaping', () => {
- it('escapes quotes in msgid and translation', () => {
+ it('escapes quotes in translation', () => {
const poContent = `
# Escaped quotes in msgid and msgstr
msgid "Changes the title to \\"%{title_param}\\"."
@@ -183,7 +183,7 @@ msgstr "Ändert den Titel in \\"%{title_param}\\"."
domain: 'app',
lang: LOCALE,
},
- 'Changes the title to \\"%{title_param}\\".': [
+ 'Changes the title to "%{title_param}".': [
'Ändert den Titel in \\"%{title_param}\\".',
],
},
@@ -191,7 +191,7 @@ msgstr "Ändert den Titel in \\"%{title_param}\\"."
});
});
- it('escapes backslashes in msgid and translation', () => {
+ it('escapes backslashes in translation', () => {
const poContent = `
# Escaped backslashes in msgid and msgstr
msgid "Example: ssh\\\\:\\\\/\\\\/"
@@ -206,7 +206,7 @@ msgstr "Beispiel: ssh\\\\:\\\\/\\\\/"
domain: 'app',
lang: LOCALE,
},
- 'Example: ssh\\\\:\\\\/\\\\/': ['Beispiel: ssh\\\\:\\\\/\\\\/'],
+ 'Example: ssh\\:\\/\\/': ['Beispiel: ssh\\\\:\\\\/\\\\/'],
},
},
});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 7cf8633d749..3f23803bbf6 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -132,6 +132,13 @@ export const MOCK_NAVIGATION = {
active: true,
count: '2,430',
},
+ epics: {
+ label: 'Epics',
+ scope: 'epics',
+ link: '/search?scope=epics&search=et',
+ active: true,
+ count: '0',
+ },
merge_requests: {
label: 'Merge requests',
scope: 'merge_requests',
@@ -496,6 +503,14 @@ export const MOCK_NAVIGATION_ITEMS = [
items: [],
},
{
+ title: 'Epics',
+ icon: 'epic',
+ link: '/search?scope=epics&search=et',
+ is_active: true,
+ pill_count: '0',
+ items: [],
+ },
+ {
title: 'Merge requests',
icon: 'merge-request',
link: '/search?scope=merge_requests&search=et',
@@ -505,7 +520,7 @@ export const MOCK_NAVIGATION_ITEMS = [
},
{
title: 'Wiki',
- icon: 'overview',
+ icon: 'book',
link: '/search?scope=wiki_blobs&search=et',
is_active: false,
pill_count: '0',
@@ -529,7 +544,7 @@ export const MOCK_NAVIGATION_ITEMS = [
},
{
title: 'Milestones',
- icon: 'tag',
+ icon: 'clock',
link: '/search?scope=milestones&search=et',
is_active: false,
pill_count: '0',
@@ -887,3 +902,5 @@ export const MOCK_FILTERED_UNAPPLIED_SELECTED_LABELS = [
parent_full_name: 'Toolbox / Gitlab Smoke Tests',
},
];
+
+export const CURRENT_SCOPE = 'blobs';
diff --git a/spec/frontend/search/sidebar/components/label_filter_spec.js b/spec/frontend/search/sidebar/components/label_filter_spec.js
index c5df374d4ef..2a5b3a96045 100644
--- a/spec/frontend/search/sidebar/components/label_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/label_filter_spec.js
@@ -92,6 +92,7 @@ describe('GlobalSearchSidebarLabelFilter', () => {
const findCheckboxFilter = () => wrapper.findAllComponents(LabelDropdownItems);
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findNoLabelsFoundMessage = () => wrapper.findComponentByTestId('no-labels-found-message');
describe('Renders correctly closed', () => {
beforeEach(async () => {
@@ -228,6 +229,33 @@ describe('GlobalSearchSidebarLabelFilter', () => {
});
});
+ describe('Renders no-labels state correctly', () => {
+ beforeEach(async () => {
+ createComponent();
+ store.commit(REQUEST_AGGREGATIONS);
+ await Vue.nextTick();
+
+ findSearchBox().vm.$emit('focusin');
+ findSearchBox().vm.$emit('input', 'ssssssss');
+ });
+
+ it('renders checkbox filter', () => {
+ expect(findCheckboxFilter().exists()).toBe(false);
+ });
+
+ it("doesn't render alert", () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it("doesn't render items", () => {
+ expect(findAllSelectedLabelsAbove().exists()).toBe(false);
+ });
+
+ it('renders no labels found text', () => {
+ expect(findNoLabelsFoundMessage().exists()).toBe(true);
+ });
+ });
+
describe('Renders error state correctly', () => {
beforeEach(async () => {
createComponent();
@@ -294,6 +322,8 @@ describe('GlobalSearchSidebarLabelFilter', () => {
describe('dropdown checkboxes work', () => {
beforeEach(async () => {
createComponent();
+ store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data);
+ await Vue.nextTick();
await findSearchBox().vm.$emit('focusin');
await Vue.nextTick();
diff --git a/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js
index 6a94da31a1b..786ad806ea6 100644
--- a/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js
@@ -7,6 +7,8 @@ import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navi
Vue.use(Vuex);
+const MOCK_NAVIGATION_ENTRIES = Object.entries(MOCK_NAVIGATION);
+
describe('ScopeLegacyNavigation', () => {
let wrapper;
@@ -55,12 +57,12 @@ describe('ScopeLegacyNavigation', () => {
});
it('renders all nav item components', () => {
- expect(findGlNavItems()).toHaveLength(9);
+ expect(findGlNavItems()).toHaveLength(MOCK_NAVIGATION_ENTRIES.length);
});
it('has all proper links', () => {
const linkAtPosition = 3;
- const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
+ const { link } = MOCK_NAVIGATION_ENTRIES[linkAtPosition][1];
expect(findGlNavItems().at(linkAtPosition).attributes('href')).toBe(link);
});
diff --git a/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js
index 4b71ff0bedc..86939bdc5d6 100644
--- a/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js
@@ -7,6 +7,8 @@ import { MOCK_QUERY, MOCK_NAVIGATION, MOCK_NAVIGATION_ITEMS } from '../../mock_d
Vue.use(Vuex);
+const MOCK_NAVIGATION_ENTRIES = Object.entries(MOCK_NAVIGATION);
+
describe('ScopeSidebarNavigation', () => {
let wrapper;
@@ -59,7 +61,7 @@ describe('ScopeSidebarNavigation', () => {
});
it('renders all nav item components', () => {
- expect(findNavItems()).toHaveLength(9);
+ expect(findNavItems()).toHaveLength(MOCK_NAVIGATION_ENTRIES.length);
});
it('has all proper links', () => {
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index 423ec6ff63b..9dc14b97ce0 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
+import { stubComponent } from 'helpers/stub_component';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
@@ -93,11 +94,20 @@ describe('GlobalSearchTopbar', () => {
});
it('dispatched correct click action', () => {
- const draweToggleSpy = jest.fn();
- wrapper.vm.$refs.markdownDrawer.toggleDrawer = draweToggleSpy;
+ const drawerToggleSpy = jest.fn();
+
+ createComponent(
+ { query: { repository_ref: '' } },
+ { elasticsearchEnabled: true, defaultBranchName: '' },
+ {
+ MarkdownDrawer: stubComponent(MarkdownDrawer, {
+ methods: { toggleDrawer: drawerToggleSpy },
+ }),
+ },
+ );
findSyntaxOptionButton().vm.$emit('click');
- expect(draweToggleSpy).toHaveBeenCalled();
+ expect(drawerToggleSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index 78d9efbd686..94882d181d3 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
+import { MOCK_GROUP, MOCK_QUERY, CURRENT_SCOPE } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { GROUPS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
@@ -37,6 +37,7 @@ describe('GroupFilter', () => {
actions: actionSpies,
getters: {
frequentGroups: () => [],
+ currentScope: () => CURRENT_SCOPE,
},
});
@@ -89,6 +90,7 @@ describe('GroupFilter', () => {
[GROUP_DATA.queryParam]: null,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
@@ -109,6 +111,7 @@ describe('GroupFilter', () => {
[GROUP_DATA.queryParam]: MOCK_GROUP.id,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index 9eda34b1633..c25d2b94027 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { MOCK_PROJECT, MOCK_QUERY } from 'jest/search/mock_data';
+import { MOCK_PROJECT, MOCK_QUERY, CURRENT_SCOPE } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
@@ -37,6 +37,7 @@ describe('ProjectFilter', () => {
actions: actionSpies,
getters: {
frequentProjects: () => [],
+ currentScope: () => CURRENT_SCOPE,
},
});
@@ -88,6 +89,7 @@ describe('ProjectFilter', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[PROJECT_DATA.queryParam]: null,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
});
@@ -107,6 +109,7 @@ describe('ProjectFilter', () => {
[GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
});
diff --git a/spec/frontend/service_desk/components/info_banner_spec.js b/spec/frontend/service_desk/components/info_banner_spec.js
new file mode 100644
index 00000000000..7487d5d8b64
--- /dev/null
+++ b/spec/frontend/service_desk/components/info_banner_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlButton } from '@gitlab/ui';
+import InfoBanner from '~/service_desk/components/info_banner.vue';
+import { infoBannerAdminNote, enableServiceDesk } from '~/service_desk/constants';
+
+describe('InfoBanner', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ serviceDeskCalloutSvgPath: 'callout.svg',
+ serviceDeskEmailAddress: 'sd@gmail.com',
+ canAdminIssues: true,
+ canEditProjectSettings: true,
+ serviceDeskSettingsPath: 'path/to/project/settings',
+ serviceDeskHelpPath: 'path/to/documentation',
+ isServiceDeskEnabled: true,
+ };
+
+ const findEnableSDButton = () => wrapper.findComponent(GlButton);
+
+ const mountComponent = (provide) => {
+ return shallowMount(InfoBanner, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ GlLink,
+ GlButton,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ describe('Service Desk email address', () => {
+ it('renders when user can admin issues and service desk is enabled', () => {
+ expect(wrapper.text()).toContain(infoBannerAdminNote);
+ expect(wrapper.text()).toContain(wrapper.vm.serviceDeskEmailAddress);
+ });
+
+ it('does not render, when user can not admin issues', () => {
+ wrapper = mountComponent({ canAdminIssues: false });
+
+ expect(wrapper.text()).not.toContain(infoBannerAdminNote);
+ expect(wrapper.text()).not.toContain(wrapper.vm.serviceDeskEmailAddress);
+ });
+
+ it('does not render, when service desk is not setup', () => {
+ wrapper = mountComponent({ isServiceDeskEnabled: false });
+
+ expect(wrapper.text()).not.toContain(infoBannerAdminNote);
+ expect(wrapper.text()).not.toContain(wrapper.vm.serviceDeskEmailAddress);
+ });
+ });
+
+ describe('Link to Service Desk settings', () => {
+ it('renders when user can edit settings and service desk is not enabled', () => {
+ wrapper = mountComponent({ isServiceDeskEnabled: false });
+
+ expect(wrapper.text()).toContain(enableServiceDesk);
+ expect(findEnableSDButton().exists()).toBe(true);
+ });
+
+ it('does not render when service desk is enabled', () => {
+ wrapper = mountComponent();
+
+ expect(wrapper.text()).not.toContain(enableServiceDesk);
+ expect(findEnableSDButton().exists()).toBe(false);
+ });
+
+ it('does not render when user cannot edit settings', () => {
+ wrapper = mountComponent({ canEditProjectSettings: false });
+
+ expect(wrapper.text()).not.toContain(enableServiceDesk);
+ expect(findEnableSDButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
new file mode 100644
index 00000000000..2ac789745aa
--- /dev/null
+++ b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
@@ -0,0 +1,151 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
+import getServiceDeskIssuesQuery from '~/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCountsQuery from '~/service_desk/queries/get_service_desk_issues_counts.query.graphql';
+import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
+import InfoBanner from '~/service_desk/components/info_banner.vue';
+import {
+ getServiceDeskIssuesQueryResponse,
+ getServiceDeskIssuesCountsQueryResponse,
+} from '../mock_data';
+
+jest.mock('@sentry/browser');
+
+describe('ServiceDeskListApp', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const defaultProvide = {
+ emptyStateSvgPath: 'empty-state.svg',
+ isProject: true,
+ isSignedIn: true,
+ fullPath: 'path/to/project',
+ isServiceDeskSupported: true,
+ hasAnyIssues: true,
+ };
+
+ const defaultQueryResponse = getServiceDeskIssuesQueryResponse;
+
+ const mockServiceDeskIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
+ const mockServiceDeskIssuesCountsQueryResponse = jest
+ .fn()
+ .mockResolvedValue(getServiceDeskIssuesCountsQueryResponse);
+
+ const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findInfoBanner = () => wrapper.findComponent(InfoBanner);
+
+ const mountComponent = ({
+ provide = {},
+ data = {},
+ serviceDeskIssuesQueryResponse = mockServiceDeskIssuesQueryResponse,
+ serviceDeskIssuesCountsQueryResponse = mockServiceDeskIssuesCountsQueryResponse,
+ stubs = {},
+ mountFn = shallowMount,
+ } = {}) => {
+ const requestHandlers = [
+ [getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponse],
+ [getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponse],
+ ];
+
+ return mountFn(ServiceDeskListApp, {
+ apolloProvider: createMockApollo(
+ requestHandlers,
+ {},
+ {
+ typePolicies: {
+ Query: {
+ fields: {
+ project: {
+ merge: true,
+ },
+ },
+ },
+ },
+ },
+ ),
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ data() {
+ return data;
+ },
+ stubs,
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ return waitForPromises();
+ });
+
+ it('fetches service desk issues and renders them in the issuable list', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ namespace: 'service-desk',
+ recentSearchesStorageKey: 'issues',
+ issuables: defaultQueryResponse.data.project.issues.nodes,
+ tabs: issuableListTabs,
+ currentTab: STATUS_OPEN,
+ tabCounts: {
+ opened: 1,
+ closed: 1,
+ all: 1,
+ },
+ });
+ });
+
+ describe('InfoBanner', () => {
+ it('renders when Service Desk is supported and has any number of issues', () => {
+ expect(findInfoBanner().exists()).toBe(true);
+ });
+
+ it('does not render, when there are no issues', async () => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: false } });
+ await waitForPromises();
+
+ expect(findInfoBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('Events', () => {
+ describe('when "click-tab" event is emitted by IssuableList', () => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
+ });
+
+ it('updates ui to the new tab', () => {
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
+ });
+ });
+ });
+
+ describe('Errors', () => {
+ describe.each`
+ error | mountOption | message
+ ${'fetching issues'} | ${'serviceDeskIssuesQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingCounts}
+ `('when there is an error $error', ({ mountOption, message }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
+ });
+ return waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(message);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/service_desk/mock_data.js b/spec/frontend/service_desk/mock_data.js
new file mode 100644
index 00000000000..17b400e8670
--- /dev/null
+++ b/spec/frontend/service_desk/mock_data.js
@@ -0,0 +1,118 @@
+export const getServiceDeskIssuesQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ issues: {
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [
+ {
+ __persist: true,
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/123456',
+ iid: '789',
+ confidential: false,
+ createdAt: '2021-05-22T04:08:01Z',
+ downvotes: 2,
+ dueDate: '2021-05-29',
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: false,
+ moved: false,
+ state: 'opened',
+ title: 'Issue title',
+ updatedAt: '2021-05-22T04:08:01Z',
+ closedAt: null,
+ upvotes: 3,
+ userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
+ webUrl: 'project/-/issues/789',
+ type: 'issue',
+ assignees: {
+ nodes: [
+ {
+ __persist: true,
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/234',
+ avatarUrl: 'avatar/url',
+ name: 'Marge Simpson',
+ username: 'msimpson',
+ webUrl: 'url/msimpson',
+ },
+ ],
+ },
+ author: {
+ __persist: true,
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/456',
+ avatarUrl: 'avatar/url',
+ name: 'GitLab Support Bot',
+ username: 'support-bot',
+ webUrl: 'url/hsimpson',
+ },
+ labels: {
+ nodes: [
+ {
+ __persist: true,
+ id: 'gid://gitlab/ProjectLabel/456',
+ color: '#333',
+ title: 'Label title',
+ description: 'Label description',
+ },
+ ],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ completedCount: 1,
+ count: 2,
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const getServiceDeskIssuesQueryEmptyResponse = {
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ issues: {
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [],
+ },
+ },
+ },
+};
+
+export const getServiceDeskIssuesCountsQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 1,
+ },
+ allIssues: {
+ count: 1,
+ },
+ },
+ },
+};
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 81b65f4f050..52355806487 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,15 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
-import { TEST_HOST } from 'helpers/test_constants';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import userDataMock from '../../user_data_mock';
-const TOOLTIP_PLACEMENT = 'bottom';
-const { name: USER_NAME } = userDataMock();
-const TEST_ISSUABLE_TYPE = 'merge_request';
+const TEST_ISSUABLE_TYPE = 'issue';
describe('AssigneeAvatarLink component', () => {
let wrapper;
@@ -17,10 +14,6 @@ describe('AssigneeAvatarLink component', () => {
function createComponent(props = {}) {
const propsData = {
user: userDataMock(),
- showLess: true,
- rootPath: TEST_HOST,
- tooltipPlacement: TOOLTIP_PLACEMENT,
- singleUser: false,
issuableType: TEST_ISSUABLE_TYPE,
...props,
};
@@ -30,7 +23,6 @@ describe('AssigneeAvatarLink component', () => {
});
}
- const findTooltipText = () => wrapper.attributes('title');
const findUserLink = () => wrapper.findComponent(GlLink);
it('has the root url present in the assigneeUrl method', () => {
@@ -50,69 +42,6 @@ describe('AssigneeAvatarLink component', () => {
);
});
- describe.each`
- issuableType | tooltipHasName | canMerge | expected
- ${'merge_request'} | ${true} | ${true} | ${USER_NAME}
- ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`}
- ${'merge_request'} | ${false} | ${true} | ${''}
- ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'}
- ${'issue'} | ${true} | ${true} | ${USER_NAME}
- ${'issue'} | ${true} | ${false} | ${USER_NAME}
- ${'issue'} | ${false} | ${true} | ${''}
- ${'issue'} | ${false} | ${false} | ${''}
- `(
- 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge',
- ({ issuableType, tooltipHasName, canMerge, expected }) => {
- beforeEach(() => {
- createComponent({
- issuableType,
- tooltipHasName,
- user: {
- ...userDataMock(),
- can_merge: canMerge,
- },
- });
- });
-
- it('sets tooltip', () => {
- expect(findTooltipText()).toBe(expected);
- });
- },
- );
-
- describe.each`
- tooltipHasName | name | availability | canMerge | expected
- ${true} | ${"Rabbit O'Hare"} | ${''} | ${true} | ${"Rabbit O'Hare"}
- ${true} | ${"Rabbit O'Hare"} | ${'Busy'} | ${false} | ${"Rabbit O'Hare (Busy) (cannot merge)"}
- ${true} | ${'Root'} | ${'Busy'} | ${false} | ${'Root (Busy) (cannot merge)'}
- ${true} | ${'Root'} | ${'Busy'} | ${true} | ${'Root (Busy)'}
- ${true} | ${'Root'} | ${''} | ${false} | ${'Root (cannot merge)'}
- ${true} | ${'Root'} | ${''} | ${true} | ${'Root'}
- ${false} | ${'Root'} | ${'Busy'} | ${false} | ${'Cannot merge'}
- ${false} | ${'Root'} | ${'Busy'} | ${true} | ${''}
- ${false} | ${'Root'} | ${''} | ${false} | ${'Cannot merge'}
- ${false} | ${'Root'} | ${''} | ${true} | ${''}
- `(
- "with name=$name tooltipHasName=$tooltipHasName and availability='$availability' and canMerge=$canMerge",
- ({ name, tooltipHasName, availability, canMerge, expected }) => {
- beforeEach(() => {
- createComponent({
- tooltipHasName,
- user: {
- ...userDataMock(),
- name,
- can_merge: canMerge,
- availability,
- },
- });
- });
-
- it(`sets tooltip to "${expected}"`, () => {
- expect(findTooltipText()).toBe(expected);
- });
- },
- );
-
it('passes the correct user id for REST API', () => {
createComponent({
tooltipHasName: true,
@@ -135,15 +64,24 @@ describe('AssigneeAvatarLink component', () => {
expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
});
- it.each`
- issuableType | userId
- ${'merge_request'} | ${undefined}
- ${'issue'} | ${'1'}
- `('sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => {
+ it('passes the correct username, cannotMerge, and CSS class for popover support', () => {
+ const moctUserData = userDataMock();
+ const { id, username } = moctUserData;
+
createComponent({
- issuableType,
+ tooltipHasName: true,
+ issuableType: 'merge_request',
+ user: { ...moctUserData, can_merge: false },
});
- expect(findUserLink().attributes('data-user-id')).toBe(userId);
+ const userLink = findUserLink();
+
+ expect(userLink.attributes()).toMatchObject({
+ 'data-user-id': `${id}`,
+ 'data-username': username,
+ 'data-cannot-merge': 'true',
+ 'data-placement': 'left',
+ });
+ expect(userLink.classes()).toContain('js-user-link');
});
});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
index d561c761c99..b2d15e76e80 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
@@ -37,27 +37,6 @@ describe('AssigneeTitle component', () => {
});
});
- describe('gutter toggle', () => {
- it('does not show toggle by default', () => {
- wrapper = createComponent({
- numberOfAssignees: 2,
- editable: false,
- });
-
- expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toBeNull();
- });
-
- it('shows toggle when showToggle is true', () => {
- wrapper = createComponent({
- numberOfAssignees: 2,
- editable: false,
- showToggle: true,
- });
-
- expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toEqual(expect.any(Object));
- });
- });
-
describe('when changing is false', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true });
diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index 1661e28abd2..65a07382ebc 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -181,7 +181,10 @@ describe('Assignee component', () => {
const userItems = findAllAvatarLinks();
expect(userItems).toHaveLength(3);
- expect(userItems.at(0).attributes('title')).toBe(users[2].name);
+ expect(userItems.at(0).attributes()).toMatchObject({
+ 'data-user-id': `${users[2].id}`,
+ 'data-username': users[2].username,
+ });
});
it('passes the sorted assignees to the collapsed-assignee-list', () => {
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 40d3d090bb4..52d68d7047e 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -194,7 +194,7 @@ describe('CollapsedAssigneeList component', () => {
${[busyUser, canMergeUser]} | ${1} | ${1} | ${`${busyUser.name} (Busy), ${canMergeUser.name} (1/2 can merge)`}
${[busyUser]} | ${1} | ${0} | ${`${busyUser.name} (Busy) (cannot merge)`}
${[canMergeUser]} | ${0} | ${1} | ${`${canMergeUser.name}`}
- ${[]} | ${0} | ${0} | ${'Assignee(s)'}
+ ${[]} | ${0} | ${0} | ${'Assignees'}
`(
'with $users.length users, $busy is busy and $canMerge that can merge',
({ users, expected }) => {
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
index a189d3656a2..a8b2db66723 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
@@ -8,6 +8,7 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.v
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import eventHub from '~/sidebar/event_hub';
import Mock from '../../mock_data';
describe('sidebar assignees', () => {
@@ -30,6 +31,9 @@ describe('sidebar assignees', () => {
});
};
+ const findAssigness = () => wrapper.findComponent(Assigness);
+ const findAssigneesRealtime = () => wrapper.findComponent(AssigneesRealtime);
+
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
mediator = new SidebarMediator(Mock.mediator);
@@ -50,18 +54,20 @@ describe('sidebar assignees', () => {
expect(mediator.saveAssignees).not.toHaveBeenCalled();
- wrapper.vm.saveAssignees();
+ eventHub.$emit('sidebar.saveAssignees');
expect(mediator.saveAssignees).toHaveBeenCalled();
});
- it('calls the mediator when "assignSelf" method is called', () => {
+ it('calls the mediator when "assignSelf" method is called', async () => {
createComponent();
+ mediator.store.isFetching.assignees = false;
+ await nextTick();
expect(mediator.assignYourself).not.toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(0);
- wrapper.vm.assignSelf();
+ await findAssigness().vm.$emit('assign-self');
expect(mediator.assignYourself).toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(1);
@@ -70,19 +76,19 @@ describe('sidebar assignees', () => {
it('hides assignees until fetched', async () => {
createComponent();
- expect(wrapper.findComponent(Assigness).exists()).toBe(false);
+ expect(findAssigness().exists()).toBe(false);
- wrapper.vm.store.isFetching.assignees = false;
+ mediator.store.isFetching.assignees = false;
await nextTick();
- expect(wrapper.findComponent(Assigness).exists()).toBe(true);
+ expect(findAssigness().exists()).toBe(true);
});
describe('when issuableType is issue', () => {
it('finds AssigneesRealtime component', () => {
createComponent();
- expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(true);
+ expect(findAssigneesRealtime().exists()).toBe(true);
});
});
@@ -90,7 +96,7 @@ describe('sidebar assignees', () => {
it('does not find AssigneesRealtime component', () => {
createComponent({ issuableType: 'MR' });
- expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(false);
+ expect(findAssigneesRealtime().exists()).toBe(false);
});
});
});
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 47f68e1fe83..da79daebb93 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -154,6 +155,13 @@ describe('IssuableLockForm', () => {
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
});
+
+ it('renders lock icon', () => {
+ const icon = findSidebarCollapseIcon().findComponent(GlIcon).props('name');
+ const expected = isLocked ? 'lock' : 'lock-open';
+
+ expect(icon).toBe(expected);
+ });
});
});
});
diff --git a/spec/frontend/sidebar/components/participants/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js
index 72d83ebeca4..2b0eac46313 100644
--- a/spec/frontend/sidebar/components/participants/participants_spec.js
+++ b/spec/frontend/sidebar/components/participants/participants_spec.js
@@ -63,6 +63,19 @@ describe('Participants component', () => {
expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants);
});
+ it('participants link has data attributes and class present for popover support', () => {
+ const numberOfLessParticipants = 2;
+ wrapper = mountComponent({ participants, numberOfLessParticipants });
+
+ const participantsLink = wrapper.find('.js-user-link');
+
+ expect(participantsLink.attributes()).toMatchObject({
+ href: `${participant.web_url}`,
+ 'data-user-id': `${participant.id}`,
+ 'data-username': `${participant.username}`,
+ });
+ });
+
it('when only showing all participants, each has an avatar', async () => {
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js
new file mode 100644
index 00000000000..79d12fa3992
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import ReviewerAvatar from '~/sidebar/components/reviewers/reviewer_avatar.vue';
+import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
+import userDataMock from '../../user_data_mock';
+
+const TEST_ISSUABLE_TYPE = 'merge_request';
+
+describe('ReviewerAvatarLink component', () => {
+ const mockUserData = {
+ ...userDataMock(),
+ webUrl: `${TEST_HOST}/root`,
+ };
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: mockUserData,
+ rootPath: TEST_HOST,
+ issuableType: TEST_ISSUABLE_TYPE,
+ ...props,
+ };
+
+ wrapper = shallowMount(ReviewerAvatarLink, {
+ propsData,
+ });
+ }
+
+ const findUserLink = () => wrapper.findComponent(GlLink);
+
+ it('has the root url present in the assigneeUrl method', () => {
+ createComponent();
+
+ expect(wrapper.attributes().href).toEqual(mockUserData.web_url);
+ });
+
+ it('renders reviewer avatar', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(ReviewerAvatar).props()).toMatchObject({
+ imgSize: 24,
+ user: mockUserData,
+ });
+ });
+
+ it('passes the correct user id, username, cannotMerge, and CSS class for popover support', () => {
+ const { id, username } = mockUserData;
+
+ createComponent({
+ tooltipHasName: true,
+ issuableType: 'merge_request',
+ user: mockUserData,
+ });
+
+ const userLink = findUserLink();
+
+ expect(userLink.attributes()).toMatchObject({
+ 'data-user-id': `${id}`,
+ 'data-username': username,
+ 'data-cannot-merge': 'true',
+ 'data-placement': 'left',
+ });
+ expect(userLink.classes()).toContain('js-user-link');
+ });
+});
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
index bee90d2b2b6..05fb75dc0fb 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -51,8 +51,7 @@ describe('SidebarSeverityWidget', () => {
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
const findEditBtn = () => wrapper.findByTestId('edit-button');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem); // First dropdown item is critical severity
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findCollapsedSeverity = () => wrapper.findComponent({ ref: 'severity' });
@@ -87,7 +86,7 @@ describe('SidebarSeverityWidget', () => {
});
createComponent({ mutationMock });
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', severity);
expect(mutationMock).toHaveBeenCalledWith({
iid,
@@ -100,7 +99,7 @@ describe('SidebarSeverityWidget', () => {
const mutationMock = jest.fn().mockRejectedValue('Something went wrong');
createComponent({ mutationMock });
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', severity);
await waitForPromises();
expect(createAlert).toHaveBeenCalled();
@@ -110,7 +109,7 @@ describe('SidebarSeverityWidget', () => {
const mutationMock = jest.fn().mockRejectedValue({});
createComponent({ mutationMock });
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', severity);
await nextTick();
expect(findLoadingIcon().exists()).toBe(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 a7c3867c359..a3b32e98506 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
@@ -1,9 +1,10 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlAlert, GlModal } from '@gitlab/ui';
+import { GlAlert, GlModal, GlFormInput, GlDatepicker, GlFormTextarea } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CreateTimelogForm from '~/sidebar/components/time_tracking/create_timelog_form.vue';
import createTimelogMutation from '~/sidebar/queries/create_timelog.mutation.graphql';
@@ -49,21 +50,19 @@ describe('Create Timelog Form', () => {
const findSaveButton = () => findModal().props('actionPrimary');
const findSaveButtonLoadingState = () => findSaveButton().attributes.loading;
const findSaveButtonDisabledState = () => findSaveButton().attributes.disabled;
+ const findGlFormInput = () => wrapper.findComponent(GlFormInput);
+ const findGlDatepicker = () => wrapper.findComponent(GlDatepicker);
+ const findGlFormTextarea = () => wrapper.findComponent(GlFormTextarea);
const submitForm = () => findForm().trigger('submit');
const mountComponent = (
- { props, data, providedProps } = {},
+ { props, providedProps } = {},
mutationResolverMock = rejectedMutationMock,
) => {
fakeApollo = createMockApollo([[createTimelogMutation, mutationResolverMock]]);
wrapper = shallowMountExtended(CreateTimelogForm, {
- data() {
- return {
- ...data,
- };
- },
provide: {
issuableType: 'issue',
...providedProps,
@@ -73,13 +72,17 @@ describe('Create Timelog Form', () => {
...props,
},
apolloProvider: fakeApollo,
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: { close: modalCloseMock },
+ }),
+ },
});
-
- wrapper.vm.$refs.modal.close = modalCloseMock;
};
afterEach(() => {
fakeApollo = null;
+ modalCloseMock.mockClear();
});
describe('save button', () => {
@@ -90,15 +93,18 @@ describe('Create Timelog Form', () => {
expect(findSaveButtonDisabledState()).toBe(true);
});
- it('is enabled and not loading when time spent is not empty', () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ it('is enabled and not loading when time spent is not empty', async () => {
+ mountComponent();
+
+ await findGlFormInput().vm.$emit('input', '2d');
expect(findSaveButtonLoadingState()).toBe(false);
expect(findSaveButtonDisabledState()).toBe(false);
});
it('is disabled and loading when the the form is submitted', async () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ mountComponent();
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -109,7 +115,8 @@ describe('Create Timelog Form', () => {
});
it('is enabled and not loading the when form is submitted but the mutation has errors', async () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ mountComponent();
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -121,7 +128,8 @@ describe('Create Timelog Form', () => {
});
it('is enabled and not loading the when form is submitted but the mutation returns errors', async () => {
- mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+ mountComponent({}, resolvedMutationWithErrorsMock);
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -145,7 +153,8 @@ describe('Create Timelog Form', () => {
});
it('closes the modal after a successful mutation', async () => {
- mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithoutErrorsMock);
+ mountComponent({}, resolvedMutationWithoutErrorsMock);
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -166,7 +175,10 @@ describe('Create Timelog Form', () => {
const spentAt = '2022-11-20T21:53:00+0000';
const summary = 'Example';
- mountComponent({ data: { timeSpent, spentAt, summary }, providedProps: { issuableType } });
+ mountComponent({ providedProps: { issuableType } });
+ await findGlFormInput().vm.$emit('input', timeSpent);
+ await findGlDatepicker().vm.$emit('input', spentAt);
+ await findGlFormTextarea().vm.$emit('input', summary);
submitForm();
@@ -187,7 +199,8 @@ describe('Create Timelog Form', () => {
});
it('shows an error if the submission fails with a handled error', async () => {
- mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+ mountComponent({}, resolvedMutationWithErrorsMock);
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -198,7 +211,8 @@ describe('Create Timelog Form', () => {
});
it('shows an error if the submission fails with an unhandled error', async () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ mountComponent();
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
diff --git a/spec/frontend/sidebar/components/time_tracking/mock_data.js b/spec/frontend/sidebar/components/time_tracking/mock_data.js
index f161ae677d0..08b6c71629a 100644
--- a/spec/frontend/sidebar/components/time_tracking/mock_data.js
+++ b/spec/frontend/sidebar/components/time_tracking/mock_data.js
@@ -148,3 +148,16 @@ export const getMrTimelogsQueryResponse = {
},
},
};
+
+export const deleteTimelogMutationResponse = {
+ data: {
+ timelogDelete: {
+ errors: [],
+ timelog: {
+ id: 'gid://gitlab/Issue/148',
+ issue: {},
+ mergeRequest: {},
+ },
+ },
+ },
+};
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 713ae83cbf1..6f25c4a10fd 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -12,6 +12,7 @@ import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.gr
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
import {
+ deleteTimelogMutationResponse,
getIssueTimelogsQueryResponse,
getMrTimelogsQueryResponse,
timelogToRemoveId,
@@ -22,7 +23,7 @@ jest.mock('~/alert');
describe('Issuable Time Tracking Report', () => {
Vue.use(VueApollo);
let wrapper;
- let fakeApollo;
+
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDeleteButton = () => wrapper.findByTestId('deleteButton');
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
@@ -30,30 +31,27 @@ describe('Issuable Time Tracking Report', () => {
const mountComponent = ({
queryHandler = successIssueQueryHandler,
+ mutationHandler,
issuableType = 'issue',
mountFunction = shallowMount,
limitToHours = false,
} = {}) => {
- fakeApollo = createMockApollo([
- [getIssueTimelogsQuery, queryHandler],
- [getMrTimelogsQuery, queryHandler],
- ]);
wrapper = extendedWrapper(
mountFunction(Report, {
+ apolloProvider: createMockApollo([
+ [getIssueTimelogsQuery, queryHandler],
+ [getMrTimelogsQuery, queryHandler],
+ [deleteTimelogMutation, mutationHandler],
+ ]),
provide: {
issuableId: 1,
issuableType,
},
propsData: { limitToHours, issuableId: '1' },
- apolloProvider: fakeApollo,
}),
);
};
- afterEach(() => {
- fakeApollo = null;
- });
-
it('should render loading spinner', () => {
mountComponent();
@@ -135,50 +133,27 @@ describe('Issuable Time Tracking Report', () => {
});
describe('when clicking on the delete timelog button', () => {
- beforeEach(() => {
- mountComponent({ mountFunction: mount });
- });
-
it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
- const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- timelogDelete: {
- errors: [],
- },
- },
- });
-
+ const mutateSpy = jest.fn().mockResolvedValue(deleteTimelogMutationResponse);
+ mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
await waitForPromises();
+
await findDeleteButton().trigger('click');
await waitForPromises();
expect(createAlert).not.toHaveBeenCalled();
- expect(mutateSpy).toHaveBeenCalledWith({
- mutation: deleteTimelogMutation,
- variables: {
- input: {
- id: timelogToRemoveId,
- },
- },
- });
+ expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
});
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
- const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
-
+ const mutateSpy = jest.fn().mockRejectedValue({});
+ mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
await waitForPromises();
+
await findDeleteButton().trigger('click');
await waitForPromises();
- expect(mutateSpy).toHaveBeenCalledWith({
- mutation: deleteTimelogMutation,
- variables: {
- input: {
- id: timelogToRemoveId,
- },
- },
- });
-
+ expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while removing the timelog.',
captureError: true,
diff --git a/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
index 846f45345e7..fd525474923 100644
--- a/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
@@ -27,6 +27,7 @@ exports[`SidebarTodo template renders component container element with proper da
label="Loading"
size="sm"
style="display: none;"
+ variant="spinner"
/>
</button>
`;
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 472a89e9b21..4385db43a4a 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -23,7 +23,6 @@ describe('Todo Button', () => {
afterEach(() => {
dispatchEventSpy = null;
- jest.clearAllMocks();
});
it('renders GlButton', () => {
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index f2003aee96e..9c12088216b 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -25,8 +25,6 @@ describe('Sidebar mediator', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
-
- jest.clearAllMocks();
});
it('assigns yourself', () => {
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 c8d972b19a3..05c1a6dd11d 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
@@ -24,7 +24,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
</div>
<div
- class="js-vue-markdown-field md-area position-relative gfm-form js-expanded"
+ class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden js-expanded"
data-uploads-path=""
>
<markdown-header-stub
@@ -83,16 +83,17 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<markdown-toolbar-stub
canattachfile="true"
markdowndocspath="help/"
- quickactionsdocspath=""
showcommenttoolbar="true"
/>
</div>
</div>
<div
- class="js-vue-md-preview md md-preview-holder gl-px-5"
+ class="js-vue-md-preview md-preview-holder gl-px-5 md"
style="display: none;"
- />
+ >
+ <div />
+ </div>
<!---->
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 70eb719f706..e2a9967f6ad 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -134,6 +134,17 @@ describe('Snippet Visibility Edit component', () => {
description: SNIPPET_VISIBILITY.private.description_project,
});
});
+
+ it('when project snippet, renders special public description', () => {
+ createComponent({ propsData: { isProjectSnippet: true }, deep: true });
+
+ expect(findRadiosData()[2]).toEqual({
+ value: VISIBILITY_LEVEL_PUBLIC_STRING,
+ icon: SNIPPET_VISIBILITY.public.icon,
+ text: SNIPPET_VISIBILITY.public.label,
+ description: SNIPPET_VISIBILITY.public.description_project,
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index fe2fd17ae4d..510a3f5b913 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -20,8 +20,12 @@ describe('CreateMenu component', () => {
const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findGlTooltip = () => wrapper.findComponent(GlTooltip);
- const createWrapper = () => {
+ const createWrapper = ({ provide = {} } = {}) => {
wrapper = shallowMountExtended(CreateMenu, {
+ provide: {
+ isImpersonating: false,
+ ...provide,
+ },
propsData: {
groups: createNewMenuGroups,
},
@@ -90,4 +94,13 @@ describe('CreateMenu component', () => {
expect(findGlTooltip().exists()).toBe(true);
});
});
+
+ it('decreases the dropdown offset when impersonating a user', () => {
+ createWrapper({ provide: { isImpersonating: true } });
+
+ expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
+ crossAxis: -115,
+ mainAxis: 4,
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
index 21d085dc0fb..85eb7e2e241 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -6,18 +6,22 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
import {
COMMAND_HANDLE,
USERS_GROUP_TITLE,
+ PATH_GROUP_TITLE,
USER_HANDLE,
+ PATH_HANDLE,
SEARCH_SCOPE,
+ MAX_ROWS,
} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
commandMapper,
linksReducer,
+ fileMapper,
} from '~/super_sidebar/components/global_search/command_palette/utils';
import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
-import { COMMANDS, LINKS, USERS } from './mock_data';
+import { COMMANDS, LINKS, USERS, FILES } from './mock_data';
const links = LINKS.reduce(linksReducer, []);
@@ -25,6 +29,8 @@ describe('CommandPaletteItems', () => {
let wrapper;
const autocompletePath = '/autocomplete';
const searchContext = { project: { id: 1 }, group: { id: 2 } };
+ const projectFilesPath = 'project/files/path';
+ const projectBlobPath = '/blob/main';
const createComponent = (props) => {
wrapper = shallowMount(CommandPaletteItems, {
@@ -42,6 +48,8 @@ describe('CommandPaletteItems', () => {
commandPaletteLinks: LINKS,
autocompletePath,
searchContext,
+ projectFilesPath,
+ projectBlobPath,
},
});
};
@@ -50,7 +58,7 @@ describe('CommandPaletteItems', () => {
const findGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
- describe('COMMANDS & LINKS', () => {
+ describe('Commands and links', () => {
it('renders all commands initially', () => {
createComponent();
const commandGroup = COMMANDS.map(commandMapper)[0];
@@ -90,7 +98,7 @@ describe('CommandPaletteItems', () => {
});
});
- describe('USERS, ISSUES, PROJECTS', () => {
+ describe('Users, issues, and projects', () => {
let mockAxios;
beforeEach(() => {
@@ -140,4 +148,83 @@ describe('CommandPaletteItems', () => {
expect(wrapper.text()).toBe('No results found');
});
});
+
+ describe('Project files', () => {
+ let mockAxios;
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ it('should request project files on first search', () => {
+ jest.spyOn(axios, 'get');
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ expect(axios.get).toHaveBeenCalledWith(projectFilesPath);
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it(`should render all items when returned number of items is less than ${MAX_ROWS}`, async () => {
+ const numberOfItems = MAX_ROWS - 1;
+ const items = FILES.slice(0, numberOfItems).map(fileMapper.bind(null, projectBlobPath));
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES.slice(0, numberOfItems));
+ jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue(items);
+
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ await waitForPromises();
+
+ expect(findGroups().at(0).props('group')).toMatchObject({
+ name: PATH_GROUP_TITLE,
+ items: items.slice(0, MAX_ROWS),
+ });
+
+ expect(findItems()).toHaveLength(numberOfItems);
+ });
+
+ it(`should render first ${MAX_ROWS} returned items when number of returned items exceeds ${MAX_ROWS}`, async () => {
+ const items = FILES.map(fileMapper.bind(null, projectBlobPath));
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES);
+ jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue(items);
+
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ await waitForPromises();
+
+ expect(findItems()).toHaveLength(MAX_ROWS);
+ expect(findGroups().at(0).props('group')).toMatchObject({
+ name: PATH_GROUP_TITLE,
+ items: items.slice(0, MAX_ROWS),
+ });
+ });
+
+ it('should display no results message when no files matched the search query', async () => {
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, []);
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+ await waitForPromises();
+ expect(wrapper.text()).toBe('No results found');
+ });
+
+ it('should not make additional server call on the search query change', async () => {
+ const searchQuery = 'gitlab-ci.yml';
+ const newSearchQuery = 'package.json';
+
+ jest.spyOn(axios, 'get');
+
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES);
+ await waitForPromises();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ searchQuery: newSearchQuery });
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
index ec65a43d549..d01e5c85741 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -131,3 +131,46 @@ export const ISSUE = {
project_name: 'Flight',
url: '/flightjs/Flight/-/issues/37',
};
+
+export const FILES = [
+ '.gitattributes',
+ '.gitignore',
+ '.gitmodules',
+ 'CHANGELOG',
+ 'CONTRIBUTING.md',
+ 'Gemfile.zip',
+ 'LICENSE',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'README',
+ 'README.md',
+ 'VERSION',
+ 'bar/branch-test.txt',
+ 'custom-highlighting/test.gitlab-custom',
+ 'encoding/feature-1.txt',
+ 'encoding/feature-2.txt',
+ 'encoding/hotfix-1.txt',
+ 'encoding/hotfix-2.txt',
+ 'encoding/iso8859.txt',
+ 'encoding/russian.rb',
+ 'encoding/test.txt',
+ 'encoding/テスト.txt',
+ 'encoding/テスト.xls',
+ 'files/flat/path/correct/content.txt',
+ 'files/html/500.html',
+ 'files/images/6049019_460s.jpg',
+ 'files/images/emoji.png',
+ 'files/images/logo-black.png',
+ 'files/images/logo-white.png',
+ 'files/images/wm.svg',
+ 'files/js/application.js',
+ 'files/js/commit.coffee',
+ 'files/lfs/lfs_object.iso',
+ 'files/markdown/ruby-style-guide.md',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb',
+ 'files/whitespace',
+ 'foo/bar/.gitkeep',
+ 'with space/README.md',
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
index 0b75787723e..ebc52e2d910 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
@@ -1,6 +1,7 @@
import {
commandMapper,
linksReducer,
+ fileMapper,
} from '~/super_sidebar/components/global_search/command_palette/utils';
import { COMMANDS, LINKS, TRANSFORMED_LINKS } from './mock_data';
@@ -16,3 +17,15 @@ describe('commandMapper', () => {
expect(COMMANDS.map(commandMapper)[0].items).toHaveLength(initialCommandsLength - 1);
});
});
+
+describe('fileMapper', () => {
+ it('should transform files', () => {
+ const file = 'file';
+ const projectBlobPath = 'project/blob/path';
+ expect(fileMapper(projectBlobPath, file)).toEqual({
+ icon: 'doc-code',
+ text: file,
+ href: `${projectBlobPath}/${file}`,
+ });
+ });
+});
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
index 9b7b9e288df..55108e116bd 100644
--- 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
@@ -12,6 +12,7 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
import {
SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
COMMON_HANDLES,
+ PATH_HANDLE,
} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
SEARCH_INPUT_DESCRIPTION,
@@ -20,8 +21,6 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
- IS_SEARCHING,
- SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/super_sidebar/components/global_search/constants';
import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
@@ -33,7 +32,6 @@ import {
MOCK_USERNAME,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
- MOCK_SEARCH_CONTEXT_FULL,
MOCK_PROJECT,
MOCK_GROUP,
} from '../mock_data';
@@ -108,7 +106,6 @@ describe('GlobalSearchModal', () => {
const findGlobalSearchModal = () => wrapper.findComponent(GlModal);
- const findGlobalSearchForm = () => wrapper.findByTestId('global-search-form');
const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findScopeToken = () => wrapper.findComponent(GlToken);
const findGlobalSearchDefaultItems = () => wrapper.findComponent(GlobalSearchDefaultItems);
@@ -203,103 +200,70 @@ describe('GlobalSearchModal', () => {
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 }) => {
+ search | hasToken
+ ${MOCK_SEARCH} | ${true}
+ ${'te'} | ${false}
+ ${'x'} | ${false}
+ ${''} | ${false}
+ `('token', ({ search, hasToken }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
- createComponent(
- { search },
- {
- searchOptions: () => searchOptions,
- },
- );
+ createComponent({ search });
findGlobalSearchInput().vm.$emit('click');
});
- it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
- searchOptions[0]?.html_id
- }"`, () => {
+ it(`${hasToken ? 'is' : 'is NOT'} rendered when search query is "${search}"`, () => {
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
- ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${[]}
- `('wrapper', ({ searchContext, search, searchOptions }) => {
+ describe.each(MOCK_SCOPED_SEARCH_OPTIONS)('token content', (searchOption) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
- createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
+ createComponent(
+ { search: MOCK_SEARCH },
+ {
+ searchOptions: () => [searchOption],
+ },
+ );
+ findGlobalSearchInput().vm.$emit('click');
});
- const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
-
- it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
- if (isSearching) {
- expect(findGlobalSearchForm().classes()).toContain(IS_SEARCHING);
- return;
- }
- if (!isSearching) {
- expect(findGlobalSearchForm().classes()).not.toContain(IS_SEARCHING);
+ it(`is correctly rendered`, () => {
+ if (searchOption.scope) {
+ expect(findScopeToken().text()).toBe(formatScopeName(searchOption.scope));
+ } else {
+ expect(findScopeToken().text()).toBe(formatScopeName(searchOption.description));
}
});
});
- });
- 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,
- },
- );
- findGlobalSearchInput().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);
- });
+ describe.each`
+ searchOptions | iconName
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${ICON_PROJECT}
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${ICON_GROUP}
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${ICON_SUBGROUP}
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false}
+ `('token', ({ searchOptions, iconName }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent(
+ { search: MOCK_SEARCH },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findGlobalSearchInput().vm.$emit('click');
+ });
- 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);
+ it(`renders ${iconName ? `"${iconName}"` : 'NO'} icon for "${
+ searchOptions[0]?.text
+ }" scope`, () => {
+ expect(
+ findScopeToken().findComponent(GlIcon).exists() &&
+ findScopeToken().findComponent(GlIcon).attributes('name'),
+ ).toBe(iconName);
+ });
});
});
@@ -319,7 +283,7 @@ describe('GlobalSearchModal', () => {
});
});
- describe.each(COMMON_HANDLES)(
+ describe.each([...COMMON_HANDLES, PATH_HANDLE])(
'when FF `command_palette` is enabled and search handle is %s',
(handle) => {
beforeEach(() => {
@@ -338,6 +302,10 @@ describe('GlobalSearchModal', () => {
SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
);
});
+
+ it('should not render the scope token', () => {
+ expect(findScopeToken().exists()).toBe(false);
+ });
},
);
});
@@ -389,33 +357,41 @@ describe('GlobalSearchModal', () => {
});
describe('Submitting a search', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('onKey-enter submits a search', () => {
+ const submitSearch = () =>
findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
- });
-
- describe('with less than min characters', () => {
+ describe('in command mode', () => {
beforeEach(() => {
- createComponent({ search: 'x' });
+ createComponent({ search: '>' }, undefined, undefined, {
+ commandPalette: true,
+ });
+ submitSearch();
});
- it('onKey-enter will NOT submit a search', () => {
- findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ it('does not submit a search', () => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+ });
+ describe('in search mode', () => {
+ it('will NOT submit a search with less than min characters', () => {
+ createComponent({ search: 'x' });
+ submitSearch();
expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
});
+
+ it('will submit a search with the sufficient number of characters', () => {
+ createComponent();
+ submitSearch();
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
});
});
});
describe('Modal events', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ search: 'searchQuery' });
});
it('should emit `shown` event when modal shown`', () => {
@@ -423,9 +399,10 @@ describe('GlobalSearchModal', () => {
expect(wrapper.emitted('shown')).toHaveLength(1);
});
- it('should emit `hidden` event when modal hidden`', () => {
- findGlobalSearchModal().vm.$emit('hidden');
+ it('should emit `hidden` event when modal hidden and clear the search input', () => {
+ findGlobalSearchModal().vm.$emit('hide');
expect(wrapper.emitted('hidden')).toHaveLength(1);
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), '');
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
index 0884fce567c..ad7e7b0b30b 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -62,20 +62,6 @@ export const MOCK_SEARCH_CONTEXT = {
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 = [
{
text: MSG_ISSUES_ASSIGNED_TO_ME,
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index 6af1172e4d8..c92f8a68678 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -104,7 +104,7 @@ describe('HelpCenter component', () => {
createWrapper({ ...sidebarData, show_tanuki_bot: true });
});
- it('shows Ask GitLab Chat with the help items', () => {
+ it('shows Ask GitLab Duo with the help items', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
expect.objectContaining({
icon: 'tanuki-ai',
@@ -115,9 +115,9 @@ describe('HelpCenter component', () => {
]);
});
- describe('when Ask GitLab Chat button is clicked', () => {
+ describe('when Ask GitLab Duo button is clicked', () => {
beforeEach(() => {
- findButton('Ask GitLab Chat').click();
+ findButton('Ask GitLab Duo').click();
});
it('sets helpCenterState.showTanukiBotChatDrawer to true', () => {
diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
index 047dc9a6599..abd9c1dc44d 100644
--- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
@@ -9,6 +9,7 @@ import SidebarPeek, {
STATE_OPEN,
STATE_WILL_CLOSE,
} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
// These are measured at runtime in the browser, but statically defined here
// since Jest does not do layout/styling.
@@ -32,6 +33,7 @@ jest.mock('~/lib/utils/css_utils', () => ({
describe('SidebarPeek component', () => {
let wrapper;
+ let trackingSpy = null;
const createComponent = () => {
wrapper = mount(SidebarPeek);
@@ -54,6 +56,11 @@ describe('SidebarPeek component', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
});
it('begins in the closed state', () => {
@@ -87,6 +94,11 @@ describe('SidebarPeek component', () => {
jest.advanceTimersByTime(1);
expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_peek', {
+ label: 'nav_hover',
+ property: 'nav_sidebar',
+ });
});
it('cancels transition will-open -> open if mouse out of peek region', () => {
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index b76c637caf4..0c785109b5e 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -19,6 +19,7 @@ import {
isCollapsed,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { sidebarData as mockSidebarData } from '../mock_data';
const initialSidebarState = { ...sidebarState };
@@ -49,6 +50,7 @@ describe('SuperSidebar component', () => {
const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
+ let trackingSpy = null;
const createWrapper = ({
provide = {},
@@ -77,6 +79,11 @@ describe('SuperSidebar component', () => {
beforeEach(() => {
Object.assign(sidebarState, initialSidebarState);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
});
describe('default', () => {
@@ -143,12 +150,20 @@ describe('SuperSidebar component', () => {
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
isCollapsed.mockReturnValue(true);
Mousetrap.trigger('mod+\\');
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(2);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
jest.spyOn(Mousetrap, 'unbind');
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
index 8bb20186e16..23b735c2773 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -7,6 +7,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants';
import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({
toggleSuperSidebarCollapsed: jest.fn(),
@@ -61,7 +62,7 @@ describe('SuperSidebarToggle component', () => {
});
});
- describe('toolip', () => {
+ describe('tooltip', () => {
it('displays collapse when expanded', () => {
createWrapper();
expect(getTooltip().title).toBe(__('Hide sidebar'));
@@ -74,15 +75,19 @@ describe('SuperSidebarToggle component', () => {
});
describe('toggle', () => {
+ let trackingSpy = null;
+
beforeEach(() => {
setHTMLFixture(`
<button class="${JS_TOGGLE_COLLAPSE_CLASS}">Hide</button>
<button class="${JS_TOGGLE_EXPAND_CLASS}">Show</button>
`);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
resetHTMLFixture();
+ unmockTracking();
});
it('collapses the sidebar and focuses the other toggle', async () => {
@@ -93,6 +98,10 @@ describe('SuperSidebarToggle component', () => {
expect(document.activeElement).toEqual(
document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
});
it('expands the sidebar and focuses the other toggle', async () => {
@@ -101,6 +110,10 @@ describe('SuperSidebarToggle component', () => {
await nextTick();
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index ae48c0f2a75..272e0237219 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -7,7 +7,6 @@ import CreateMenu from '~/super_sidebar/components/create_menu.vue';
import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
-import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
@@ -19,10 +18,9 @@ describe('UserBar component', () => {
let wrapper;
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
- const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
- const findIssuesCounter = () => findCounter(0);
- const findMRsCounter = () => findCounter(1);
- const findTodosCounter = () => findCounter(2);
+ const findIssuesCounter = () => wrapper.findByTestId('issues-shortcut-button');
+ const findMRsCounter = () => wrapper.findByTestId('merge-requests-shortcut-button');
+ const findTodosCounter = () => wrapper.findByTestId('todos-shortcut-button');
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const findBrandLogo = () => wrapper.findComponent(BrandLogo);
const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index f0f18ca9185..662677be40f 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -20,7 +20,7 @@ describe('UserMenu component', () => {
const closeDropdownSpy = jest.fn();
- const createWrapper = (userDataChanges = {}, stubs = {}) => {
+ const createWrapper = (userDataChanges = {}, stubs = {}, provide = {}) => {
wrapper = mountExtended(UserMenu, {
propsData: {
data: {
@@ -35,6 +35,8 @@ describe('UserMenu component', () => {
},
provide: {
toggleNewNavEndpoint,
+ isImpersonating: false,
+ ...provide,
},
});
@@ -50,6 +52,15 @@ describe('UserMenu component', () => {
});
});
+ it('decreases the dropdown offset when impersonating a user', () => {
+ createWrapper(null, null, { isImpersonating: true });
+
+ expect(findDropdown().props('dropdownOffset')).toEqual({
+ crossAxis: -179,
+ mainAxis: 4,
+ });
+ });
+
describe('Toggle button', () => {
let toggle;
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
index 6e3b18d3107..bd02f3c17e3 100644
--- a/spec/frontend/super_sidebar/components/user_name_group_spec.js
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -91,7 +91,7 @@ describe('UserNameGroup component', () => {
});
it('should render status message', () => {
- expect(findUserStatus().text()).toContain(userMenuMockData.status.message);
+ expect(findUserStatus().html()).toContain(userMenuMockData.status.message_html);
});
it("sets the tooltip's target to the status container", () => {
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index a3a74f7aac8..72c67e34038 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -126,6 +126,7 @@ export const userMenuMockStatus = {
customized: false,
emoji: 'art',
message: 'Working on user menu in super sidebar',
+ message_html: '<gl-emoji></gl-emoji> Working on user menu in super sidebar',
availability: 'busy',
clear_after: '2023-02-09 20:06:35 UTC',
};
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
index 771d1f07fea..9388d837186 100644
--- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
+++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
@@ -11,8 +11,10 @@ import {
findPage,
bindSuperSidebarCollapsedEvents,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
const { xl, sm } = breakpoints;
+let trackingSpy = null;
jest.mock('~/lib/utils/common_utils', () => ({
getCookie: jest.fn(),
@@ -27,6 +29,15 @@ const pageHasCollapsedClass = (hasClass) => {
}
};
+const tracksCollapse = (shouldTrack) => {
+ if (shouldTrack) {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'browser_resize',
+ property: 'nav_sidebar',
+ });
+ }
+};
+
describe('Super Sidebar Collapsed State Manager', () => {
beforeEach(() => {
setHTMLFixture(`
@@ -34,10 +45,12 @@ describe('Super Sidebar Collapsed State Manager', () => {
<aside class="super-sidebar"></aside>
</div>
`);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
resetHTMLFixture();
+ unmockTracking();
});
describe('toggleSuperSidebarCollapsed', () => {
@@ -109,14 +122,20 @@ describe('Super Sidebar Collapsed State Manager', () => {
});
it.each`
- initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize
- ${xl} | ${sm} | ${false} | ${true}
- ${sm} | ${xl} | ${true} | ${false}
- ${xl} | ${xl} | ${false} | ${false}
- ${sm} | ${sm} | ${true} | ${true}
+ initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize | sendsTrackingEvent
+ ${xl} | ${sm} | ${false} | ${true} | ${true}
+ ${sm} | ${xl} | ${true} | ${false} | ${false}
+ ${xl} | ${xl} | ${false} | ${false} | ${false}
+ ${sm} | ${sm} | ${true} | ${true} | ${false}
`(
'when changing width from $initialWindowWidth to $updatedWindowWidth expect page to have collapsed class before resize to be $hasClassBeforeResize and after resize to be $hasClassAfterResize',
- ({ initialWindowWidth, updatedWindowWidth, hasClassBeforeResize, hasClassAfterResize }) => {
+ ({
+ initialWindowWidth,
+ updatedWindowWidth,
+ hasClassBeforeResize,
+ hasClassAfterResize,
+ sendsTrackingEvent,
+ }) => {
getCookie.mockReturnValue(undefined);
window.innerWidth = initialWindowWidth;
initSuperSidebarCollapsedState();
@@ -129,6 +148,7 @@ describe('Super Sidebar Collapsed State Manager', () => {
window.dispatchEvent(new Event('resize'));
pageHasCollapsedClass(hasClassAfterResize);
+ tracksCollapse(sendsTrackingEvent);
},
);
});
diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js
index 8ec9925563a..5a3104fad9b 100644
--- a/spec/frontend/tags/components/delete_tag_modal_spec.js
+++ b/spec/frontend/tags/components/delete_tag_modal_spec.js
@@ -11,6 +11,9 @@ let wrapper;
const tagName = 'test-tag';
const path = '/path/to/tag';
const isProtected = false;
+const modalHideSpy = jest.fn();
+const modalShowSpy = jest.fn();
+const formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
const createComponent = (data = {}) => {
wrapper = extendedWrapper(
@@ -27,6 +30,10 @@ const createComponent = (data = {}) => {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: {
+ hide: modalHideSpy,
+ show: modalShowSpy,
+ },
}),
GlButton,
GlFormInput,
@@ -61,32 +68,26 @@ describe('Delete tag modal', () => {
});
it('submits the form when the delete button is clicked', () => {
- const submitFormSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
-
findDeleteButton().trigger('click');
expect(findForm().attributes('action')).toBe(path);
- expect(submitFormSpy).toHaveBeenCalled();
+ expect(formSubmitSpy).toHaveBeenCalledTimes(1);
});
it('calls show on the modal when a `openModal` event is received through the event hub', () => {
- const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
-
eventHub.$emit('openModal', {
isProtected,
tagName,
path,
});
- expect(showSpy).toHaveBeenCalled();
+ expect(modalShowSpy).toHaveBeenCalled();
});
it('calls hide on the modal when cancel button is clicked', () => {
- const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
-
findCancelButton().trigger('click');
- expect(closeModalSpy).toHaveBeenCalled();
+ expect(modalHideSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 7f321495d72..f9eb201eb5c 100644
--- a/spec/frontend/token_access/outbound_token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -6,7 +6,6 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import waitForPromises from 'helpers/wait_for_promises';
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';
import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql';
@@ -15,7 +14,6 @@ import {
enabledJobTokenScope,
disabledJobTokenScope,
projectsWithScope,
- addProjectSuccess,
removeProjectSuccess,
updateScopeSuccess,
} from './mock_data';
@@ -34,16 +32,13 @@ describe('TokenAccess component', () => {
const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope);
const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope);
const getProjectsWithScopeHandler = jest.fn().mockResolvedValue(projectsWithScope);
- const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess);
const removeProjectSuccessHandler = jest.fn().mockResolvedValue(removeProjectSuccess);
const updateScopeSuccessHandler = jest.fn().mockResolvedValue(updateScopeSuccess);
const failureHandler = jest.fn().mockRejectedValue(error);
const findToggle = () => wrapper.findComponent(GlToggle);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
- const findTokenDisabledAlert = () => wrapper.findByTestId('token-disabled-alert');
const findDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
const findProjectPathInput = () => wrapper.findByTestId('project-path-input');
@@ -51,19 +46,10 @@ describe('TokenAccess component', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (
- requestHandlers,
- mountFn = shallowMountExtended,
- frozenOutboundJobTokenScopes = false,
- frozenOutboundJobTokenScopesOverride = false,
- ) => {
+ const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
wrapper = mountFn(OutboundTokenAccess, {
provide: {
fullPath: projectPath,
- glFeatures: {
- frozenOutboundJobTokenScopes,
- frozenOutboundJobTokenScopesOverride,
- },
},
apolloProvider: createMockApolloProvider(requestHandlers),
data() {
@@ -141,19 +127,6 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(findToggle().props('value')).toBe(true);
- expect(findTokenDisabledAlert().exists()).toBe(false);
- });
-
- it('the toggle is off and the alert is visible', async () => {
- createComponent([
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ]);
-
- await waitForPromises();
-
- expect(findToggle().props('value')).toBe(false);
- expect(findTokenDisabledAlert().exists()).toBe(true);
});
describe('update ci job token scope', () => {
@@ -196,48 +169,37 @@ describe('TokenAccess component', () => {
expect(createAlert).toHaveBeenCalledWith({ message });
});
});
- });
- describe('add project', () => {
- it('calls add project mutation', async () => {
+ it('the toggle is off and the deprecation alert is visible', async () => {
createComponent(
[
- [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- [addProjectCIJobTokenScopeMutation, addProjectSuccessHandler],
],
- mountExtended,
+ shallowMountExtended,
+ true,
);
await waitForPromises();
- findAddProjectBtn().trigger('click');
-
- expect(addProjectSuccessHandler).toHaveBeenCalledWith({
- input: {
- projectPath,
- targetProjectPath: 'root/test',
- },
- });
+ expect(findToggle().props('value')).toBe(false);
+ expect(findToggle().props('disabled')).toBe(true);
+ expect(findDeprecationAlert().exists()).toBe(true);
});
- it('add project handles error correctly', async () => {
+ it('contains a warning message about disabling the current configuration', async () => {
createComponent(
[
- [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- [addProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
+ true,
);
await waitForPromises();
- findAddProjectBtn().trigger('click');
-
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({ message });
+ expect(findToggle().text()).toContain('Disabling this feature is a permanent change.');
});
});
@@ -284,58 +246,21 @@ describe('TokenAccess component', () => {
});
});
- describe('with the frozenOutboundJobTokenScopes feature flag enabled', () => {
- describe('toggle', () => {
- it('the toggle is off and the deprecation alert is visible', async () => {
- createComponent(
- [
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ],
- shallowMountExtended,
- true,
- );
-
- await waitForPromises();
-
- expect(findToggle().props('value')).toBe(false);
- expect(findToggle().props('disabled')).toBe(true);
- expect(findDeprecationAlert().exists()).toBe(true);
- expect(findTokenDisabledAlert().exists()).toBe(false);
- });
-
- it('contains a warning message about disabling the current configuration', async () => {
- createComponent(
- [
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ],
- mountExtended,
- true,
- );
-
- await waitForPromises();
-
- expect(findToggle().text()).toContain('Disabling this feature is a permanent change.');
- });
- });
-
- describe('adding a new project', () => {
- it('disables the input to add new projects', async () => {
- createComponent(
- [
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ],
- mountExtended,
- true,
- false,
- );
+ describe('adding a new project', () => {
+ it('disables the input to add new projects', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ mountExtended,
+ true,
+ false,
+ );
- await waitForPromises();
+ await waitForPromises();
- expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
- });
+ expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
});
});
});
diff --git a/spec/frontend/tracing/components/tracing_empty_state_spec.js b/spec/frontend/tracing/components/tracing_empty_state_spec.js
new file mode 100644
index 00000000000..c3df187e1c5
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_empty_state_spec.js
@@ -0,0 +1,44 @@
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
+
+describe('TracingEmptyState', () => {
+ let wrapper;
+
+ const findEnableButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(TracingEmptyState, {
+ propsData: {
+ enableTracing: jest.fn(),
+ },
+ stubs: { GlButton },
+ });
+ });
+
+ it('renders the component properly', () => {
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('displays the correct title', () => {
+ const { title } = wrapper.findComponent(GlEmptyState).props();
+ expect(title).toBe('Get started with Tracing');
+ });
+
+ it('displays the correct description', () => {
+ const description = wrapper.find('span').text();
+ expect(description).toBe('Monitor your applications with GitLab Distributed Tracing.');
+ });
+
+ it('displays the enable button', () => {
+ const enableButton = findEnableButton();
+ expect(enableButton.exists()).toBe(true);
+ expect(enableButton.text()).toBe('Enable');
+ });
+
+ it('calls enableTracing method when enable button is clicked', () => {
+ findEnableButton().vm.$emit('click');
+
+ expect(wrapper.props().enableTracing).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/tracing/components/tracing_list_spec.js b/spec/frontend/tracing/components/tracing_list_spec.js
new file mode 100644
index 00000000000..183578cff31
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_list_spec.js
@@ -0,0 +1,131 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TracingList from '~/tracing/components/tracing_list.vue';
+import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
+import TracingTableList from '~/tracing/components/tracing_table_list.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+describe('TracingList', () => {
+ let wrapper;
+ let observabilityClientMock;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(TracingEmptyState);
+ const findTableList = () => wrapper.findComponent(TracingTableList);
+
+ const mountComponent = async () => {
+ wrapper = shallowMountExtended(TracingList, {
+ propsData: {
+ observabilityClient: observabilityClientMock,
+ stubs: {
+ GlLoadingIcon: true,
+ TracingEmptyState: true,
+ TracingTableList: true,
+ },
+ },
+ });
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ observabilityClientMock = {
+ isTracingEnabled: jest.fn(),
+ enableTraces: jest.fn(),
+ fetchTraces: jest.fn(),
+ };
+ });
+
+ it('renders the loading indicator while checking if tracing is enabled', () => {
+ mountComponent();
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
+ });
+
+ describe('when tracing is enabled', () => {
+ const mockTraces = ['trace1', 'trace2'];
+ beforeEach(async () => {
+ observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(true);
+ observabilityClientMock.fetchTraces.mockResolvedValueOnce(mockTraces);
+
+ await mountComponent();
+ });
+ it('fetches the traces and renders the trace list', () => {
+ expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
+ expect(observabilityClientMock.fetchTraces).toHaveBeenCalled();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTableList().exists()).toBe(true);
+ expect(findTableList().props('traces')).toBe(mockTraces);
+ });
+
+ it('calls fetchTraces method when TracingTableList emits reload event', () => {
+ observabilityClientMock.fetchTraces.mockClear();
+ observabilityClientMock.fetchTraces.mockResolvedValueOnce(['trace1']);
+
+ findTableList().vm.$emit('reload');
+
+ expect(observabilityClientMock.fetchTraces).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when tracing is not enabled', () => {
+ beforeEach(async () => {
+ observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(false);
+ observabilityClientMock.fetchTraces.mockResolvedValueOnce([]);
+
+ await mountComponent();
+ });
+
+ it('renders TracingEmptyState', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('set enableTracing as TracingEmptyState enable-tracing callback', () => {
+ findEmptyState().props('enableTracing')();
+
+ expect(observabilityClientMock.enableTraces).toHaveBeenCalled();
+ });
+ });
+
+ describe('error handling', () => {
+ it('if isTracingEnabled fails, it renders an alert and empty page', async () => {
+ observabilityClientMock.isTracingEnabled.mockRejectedValueOnce('error');
+
+ await mountComponent();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load page.' });
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTableList().exists()).toBe(false);
+ });
+
+ it('if fetchTraces fails, it renders an alert and empty list', async () => {
+ observabilityClientMock.fetchTraces.mockRejectedValueOnce('error');
+ observabilityClientMock.isTracingEnabled.mockReturnValueOnce(true);
+
+ await mountComponent();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load traces.' });
+ expect(findTableList().exists()).toBe(true);
+ expect(findTableList().props('traces')).toEqual([]);
+ });
+
+ it('if enableTraces fails, it renders an alert and empty-state', async () => {
+ observabilityClientMock.isTracingEnabled.mockReturnValueOnce(false);
+ observabilityClientMock.enableTraces.mockRejectedValueOnce('error');
+
+ await mountComponent();
+
+ findEmptyState().props('enableTracing')();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to enable tracing.' });
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findTableList().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/tracing/components/tracing_table_list_spec.js b/spec/frontend/tracing/components/tracing_table_list_spec.js
new file mode 100644
index 00000000000..773b3eb8ed2
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_table_list_spec.js
@@ -0,0 +1,63 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import TracingTableList from '~/tracing/components/tracing_table_list.vue';
+
+describe('TracingTableList', () => {
+ let wrapper;
+ const mockTraces = [
+ {
+ timestamp: '2023-07-10T15:02:30.677538Z',
+ service_name: 'tracegen',
+ operation: 'lets-go',
+ duration: 150,
+ },
+ {
+ timestamp: '2023-07-10T15:02:30.677538Z',
+ service_name: 'tracegen',
+ operation: 'lets-go',
+ duration: 200,
+ },
+ ];
+
+ const mountComponent = ({ traces = mockTraces } = {}) => {
+ wrapper = mountExtended(TracingTableList, {
+ propsData: {
+ traces,
+ },
+ });
+ };
+
+ const getRows = () => wrapper.findComponent({ name: 'GlTable' }).find('tbody').findAll('tr');
+
+ const getCells = (trIdx) => getRows().at(trIdx).findAll('td');
+
+ const getCell = (trIdx, tdIdx) => {
+ return getCells(trIdx).at(tdIdx);
+ };
+
+ it('renders traces as table', () => {
+ mountComponent();
+
+ const rows = wrapper.findAll('table tbody tr');
+
+ expect(rows.length).toBe(mockTraces.length);
+
+ mockTraces.forEach((trace, i) => {
+ expect(getCells(i).length).toBe(4);
+ expect(getCell(i, 0).text()).toBe(trace.timestamp);
+ expect(getCell(i, 1).text()).toBe(trace.service_name);
+ expect(getCell(i, 2).text()).toBe(trace.operation);
+ expect(getCell(i, 3).text()).toBe(`${trace.duration} ms`);
+ });
+ });
+
+ it('renders the empty state when no traces are provided', () => {
+ mountComponent({ traces: [] });
+
+ expect(getCell(0, 0).text()).toContain('No traces to display');
+ const link = getCell(0, 0).findComponent({ name: 'GlLink' });
+ expect(link.text()).toBe('Check again');
+
+ link.trigger('click');
+ expect(wrapper.emitted('reload')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/tracing/list_index_spec.js b/spec/frontend/tracing/list_index_spec.js
new file mode 100644
index 00000000000..a5759035c2f
--- /dev/null
+++ b/spec/frontend/tracing/list_index_spec.js
@@ -0,0 +1,37 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ListIndex from '~/tracing/list_index.vue';
+import TracingList from '~/tracing/components/tracing_list.vue';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+
+describe('ListIndex', () => {
+ const props = {
+ oauthUrl: 'https://example.com/oauth',
+ tracingUrl: 'https://example.com/tracing',
+ provisioningUrl: 'https://example.com/provisioning',
+ };
+
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMountExtended(ListIndex, {
+ propsData: props,
+ });
+ };
+
+ it('renders ObservabilityContainer component', () => {
+ mountComponent();
+
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.exists()).toBe(true);
+ expect(observabilityContainer.props('oauthUrl')).toBe(props.oauthUrl);
+ expect(observabilityContainer.props('tracingUrl')).toBe(props.tracingUrl);
+ expect(observabilityContainer.props('provisioningUrl')).toBe(props.provisioningUrl);
+ });
+
+ it('renders TracingList component inside ObservabilityContainer', () => {
+ mountComponent();
+
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.findComponent(TracingList).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js
new file mode 100644
index 00000000000..ad2ffa7cef4
--- /dev/null
+++ b/spec/frontend/tracking/internal_events_spec.js
@@ -0,0 +1,100 @@
+import API from '~/api';
+import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import InternalEvents from '~/tracking/internal_events';
+import { GITLAB_INTERNAL_EVENT_CATEGORY, SERVICE_PING_SCHEMA } from '~/tracking/constants';
+import * as utils from '~/tracking/utils';
+import { Tracker } from '~/tracking/tracker';
+
+jest.mock('~/api', () => ({
+ trackRedisHllUserEvent: jest.fn(),
+}));
+
+jest.mock('~/tracking/utils', () => ({
+ ...jest.requireActual('~/tracking/utils'),
+ getInternalEventHandlers: jest.fn(),
+}));
+
+Tracker.enabled = jest.fn();
+
+describe('InternalEvents', () => {
+ describe('track_event', () => {
+ it('track_event calls trackRedisHllUserEvent with correct arguments', () => {
+ const event = 'TestEvent';
+
+ InternalEvents.track_event(event);
+
+ expect(API.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
+ expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event);
+ });
+
+ it('track_event calls tracking.event functions with correct arguments', () => {
+ const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn);
+
+ const event = 'TestEvent';
+
+ InternalEvents.track_event(event);
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
+ context: {
+ schema: SERVICE_PING_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: 'redis_hll',
+ },
+ },
+ });
+ });
+ });
+
+ describe('mixin', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const Component = {
+ render() {},
+ mixins: [InternalEvents.mixin()],
+ };
+ wrapper = shallowMountExtended(Component);
+ });
+
+ it('this.track_event function calls InternalEvent`s track function with an event', () => {
+ const event = 'TestEvent';
+ const trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
+
+ wrapper.vm.track_event(event);
+
+ expect(trackEventSpy).toHaveBeenCalledTimes(1);
+ expect(trackEventSpy).toHaveBeenCalledWith(event);
+ });
+ });
+
+ describe('bindInternalEventDocument', () => {
+ it('should not bind event handlers if tracker is not enabled', () => {
+ Tracker.enabled.mockReturnValue(false);
+ const result = InternalEvents.bindInternalEventDocument();
+ expect(result).toEqual([]);
+ expect(utils.getInternalEventHandlers).not.toHaveBeenCalled();
+ });
+
+ it('should not bind event handlers if already bound', () => {
+ Tracker.enabled.mockReturnValue(true);
+ document.internalEventsTrackingBound = true;
+ const result = InternalEvents.bindInternalEventDocument();
+ expect(result).toEqual([]);
+ expect(utils.getInternalEventHandlers).not.toHaveBeenCalled();
+ });
+
+ it('should bind event handlers when not bound yet', () => {
+ Tracker.enabled.mockReturnValue(true);
+ document.internalEventsTrackingBound = false;
+ const addEventListenerMock = jest.spyOn(document, 'addEventListener');
+
+ const result = InternalEvents.bindInternalEventDocument();
+
+ expect(addEventListenerMock).toHaveBeenCalledWith('click', expect.any(Function));
+ expect(result).toEqual({ name: 'click', func: expect.any(Function) });
+ });
+ });
+});
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index c23790bb589..55ce8039399 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -59,7 +59,6 @@ describe('Tracking', () => {
window.doNotTrack = undefined;
navigator.doNotTrack = undefined;
navigator.msDoNotTrack = undefined;
- jest.clearAllMocks();
});
it('tracks to snowplow (our current tracking system)', () => {
diff --git a/spec/frontend/tracking/utils_spec.js b/spec/frontend/tracking/utils_spec.js
index d6f2c5095b4..7ba65cce15d 100644
--- a/spec/frontend/tracking/utils_spec.js
+++ b/spec/frontend/tracking/utils_spec.js
@@ -4,6 +4,8 @@ import {
addExperimentContext,
addReferrersCacheEntry,
filterOldReferrersCacheEntries,
+ InternalEventHandler,
+ createInternalEventPayload,
} from '~/tracking/utils';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants';
@@ -95,5 +97,40 @@ describe('~/tracking/utils', () => {
expect(cache[0].timestamp).toBeDefined();
});
});
+
+ describe('createInternalEventPayload', () => {
+ it('should return event name from element', () => {
+ const mockEl = { dataset: { eventTracking: 'click' } };
+ const result = createInternalEventPayload(mockEl);
+ expect(result).toEqual('click');
+ });
+ });
+
+ describe('InternalEventHandler', () => {
+ it.each([
+ ['should call the provided function with the correct event payload', 'click', true],
+ [
+ 'should not call the provided function if the closest matching element is not found',
+ null,
+ false,
+ ],
+ ])('%s', (_, payload, shouldCallFunc) => {
+ const mockFunc = jest.fn();
+ const mockEl = payload ? { dataset: { eventTracking: payload } } : null;
+ const mockEvent = {
+ target: {
+ closest: jest.fn().mockReturnValue(mockEl),
+ },
+ };
+
+ InternalEventHandler(mockEvent, mockFunc);
+
+ if (shouldCallFunc) {
+ expect(mockFunc).toHaveBeenCalledWith(payload);
+ } else {
+ expect(mockFunc).not.toHaveBeenCalled();
+ }
+ });
+ });
});
});
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 2662711076b..7fef20c900e 100644
--- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
@@ -19,7 +19,7 @@ function findStorageTypeUsagesSerialized() {
.wrappers.map((wp) => wp.element.style.flex);
}
-describe('Storage Counter usage graph component', () => {
+describe('UsageGraph', () => {
beforeEach(() => {
data = {
rootStorageStatistics: {
@@ -29,7 +29,6 @@ describe('Storage Counter usage graph component', () => {
containerRegistrySize: 2500,
lfsObjectsSize: 2000,
buildArtifactsSize: 700,
- pipelineArtifactsSize: 300,
snippetsSize: 2000,
storageSize: 17000,
},
@@ -43,7 +42,6 @@ describe('Storage Counter usage graph component', () => {
const {
buildArtifactsSize,
- pipelineArtifactsSize,
lfsObjectsSize,
packagesSize,
containerRegistrySize,
@@ -69,9 +67,6 @@ describe('Storage Counter usage graph component', () => {
expect(types.at(6).text()).toMatchInterpolatedText(
`Job artifacts ${numberToHumanSize(buildArtifactsSize)}`,
);
- expect(types.at(7).text()).toMatchInterpolatedText(
- `Pipeline artifacts ${numberToHumanSize(pipelineArtifactsSize)}`,
- );
});
describe('when storage type is not used', () => {
@@ -111,7 +106,6 @@ describe('Storage Counter usage graph component', () => {
'0.11764705882352941',
'0.11764705882352941',
'0.041176470588235294',
- '0.01764705882352941',
]);
});
});
@@ -131,7 +125,6 @@ describe('Storage Counter usage graph component', () => {
'0.11764705882352941',
'0.11764705882352941',
'0.041176470588235294',
- '0.01764705882352941',
]);
});
});
diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js
index 8a7f941151b..452fa83b9a7 100644
--- a/spec/frontend/usage_quotas/storage/mock_data.js
+++ b/spec/frontend/usage_quotas/storage/mock_data.js
@@ -5,7 +5,7 @@ export const mockEmptyResponse = { data: { project: null } };
export const projectData = {
storage: {
- totalUsage: '13.8 MiB',
+ totalUsage: '13.4 MiB',
storageTypes: [
{
storageType: {
@@ -29,15 +29,6 @@ export const projectData = {
},
{
storageType: {
- id: 'pipelineArtifacts',
- name: 'Pipeline artifacts',
- description: 'Pipeline artifacts created by CI/CD.',
- helpPath: '/pipeline-artifacts',
- },
- value: 400000,
- },
- {
- storageType: {
id: 'lfsObjects',
name: 'LFS',
description: 'Audio samples, videos, datasets, and graphics.',
@@ -93,7 +84,6 @@ export const projectHelpLinks = {
containerRegistry: '/container_registry',
usageQuotas: '/usage-quotas',
buildArtifacts: '/build-artifacts',
- pipelineArtifacts: '/pipeline-artifacts',
lfsObjects: '/lsf-objects',
packages: '/packages',
repository: '/repository',
diff --git a/spec/frontend/users/profile/actions/components/user_actions_app_spec.js b/spec/frontend/users/profile/actions/components/user_actions_app_spec.js
new file mode 100644
index 00000000000..d27962440ee
--- /dev/null
+++ b/spec/frontend/users/profile/actions/components/user_actions_app_spec.js
@@ -0,0 +1,38 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserActionsApp from '~/users/profile/actions/components/user_actions_app.vue';
+
+describe('User Actions App', () => {
+ let wrapper;
+
+ const USER_ID = 'test-id';
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = mountExtended(UserActionsApp, {
+ propsData: {
+ userId: USER_ID,
+ ...propsData,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findActions = () => wrapper.findAllByTestId('disclosure-dropdown-item');
+ const findAction = (position = 0) => findActions().at(position);
+
+ it('shows dropdown', () => {
+ createWrapper();
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('shows actions correctly', () => {
+ createWrapper();
+ expect(findActions()).toHaveLength(1);
+ });
+
+ it('shows copy user id action', () => {
+ createWrapper();
+ expect(findAction().text()).toBe(`Copy user ID: ${USER_ID}`);
+ expect(findAction().findComponent('button').attributes('data-clipboard-text')).toBe(USER_ID);
+ });
+});
diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js
index 6eba9465c80..fe43f8f2617 100644
--- a/spec/frontend/vue_compat_test_setup.js
+++ b/spec/frontend/vue_compat_test_setup.js
@@ -76,9 +76,77 @@ if (global.document) {
Vue.configureCompat(compatConfig);
installVTUCompat(VTU, fullCompatConfig, compatH);
+
+ jest.mock('vue', () => {
+ const actualVue = jest.requireActual('vue');
+ actualVue.configureCompat(compatConfig);
+ return actualVue;
+ });
+
+ jest.mock('@vue/test-utils', () => {
+ const actualVTU = jest.requireActual('@vue/test-utils');
+
+ return {
+ ...actualVTU,
+ RouterLinkStub: {
+ ...actualVTU.RouterLinkStub,
+ render() {
+ const { default: defaultSlot } = this.$slots ?? {};
+ const defaultSlotFn =
+ defaultSlot && typeof defaultSlot !== 'function' ? () => defaultSlot : defaultSlot;
+ return actualVTU.RouterLinkStub.render.call({
+ $slots: defaultSlot ? { default: defaultSlotFn } : undefined,
+ custom: this.custom,
+ });
+ },
+ },
+ };
+ });
+
+ jest.mock('portal-vue', () => ({
+ __esModule: true,
+ default: {
+ install: jest.fn(),
+ },
+ Portal: {},
+ PortalTarget: {},
+ MountingPortal: {
+ template: '<h1>MOUNTING-PORTAL</h1>',
+ },
+ Wormhole: {},
+ }));
+
VTU.config.global.renderStubDefaultSlot = true;
const noop = () => {};
+ const invalidProperties = new Set();
+
+ const getDescriptor = (root, prop) => {
+ let obj = root;
+ while (obj != null) {
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
+ if (desc) {
+ return desc;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ return null;
+ };
+
+ const isPropertyValidOnDomNode = (prop) => {
+ if (invalidProperties.has(prop)) {
+ return false;
+ }
+
+ const domNode = document.createElement('anonymous-stub');
+ const descriptor = getDescriptor(domNode, prop);
+ if (descriptor && descriptor.get && !descriptor.set) {
+ invalidProperties.add(prop);
+ return false;
+ }
+
+ return true;
+ };
VTU.config.plugins.createStubs = ({ name, component: rawComponent, registerStub }) => {
const component = unwrapLegacyVueExtendComponent(rawComponent);
@@ -126,7 +194,11 @@ if (global.document) {
.filter(Boolean)
: renderSlotByName('default');
- return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, this.$props, slotContents);
+ const props = Object.fromEntries(
+ Object.entries(this.$props).filter(([prop]) => isPropertyValidOnDomNode(prop)),
+ );
+
+ return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, props, slotContents);
},
});
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 e4febda1daa..b0f9f123950 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
@@ -1,22 +1,22 @@
-import { GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
{
title: 'Commit 1',
- short_id: '78d5b7',
+ shortId: '78d5b7',
message: 'Update test.txt',
},
{
title: 'Commit 2',
- short_id: '34cbe28b',
+ shortId: '34cbe28b',
message: 'Fixed test',
},
{
title: 'Commit 3',
- short_id: 'fa42932a',
+ shortId: 'fa42932a',
message: 'Added changelog',
},
];
@@ -25,10 +25,14 @@ describe('Commits message dropdown component', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMount(CommitMessageDropdown, {
+ wrapper = mount(CommitMessageDropdown, {
propsData: {
commits,
},
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
});
};
@@ -36,7 +40,7 @@ describe('Commits message dropdown component', () => {
createComponent();
});
- const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownElements = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {
@@ -48,10 +52,9 @@ describe('Commits message dropdown component', () => {
expect(findFirstDropdownElement().text()).toContain('Commit 1');
});
- it('should emit a commit title on selecting commit', async () => {
- findFirstDropdownElement().vm.$emit('click');
+ it('should emit a commit title on selecting commit', () => {
+ findFirstDropdownElement().find('button').trigger('click');
- await nextTick();
expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']);
});
});
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 38e5422325a..e1c88d7d3b6 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import MrWidgetFailedToMerge from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
@@ -8,17 +8,14 @@ describe('MRWidgetFailedToMerge', () => {
const dummyIntervalId = 1337;
let wrapper;
- const createComponent = (props = {}, data = {}) => {
- wrapper = shallowMount(MrWidgetFailedToMerge, {
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(MrWidgetFailedToMerge, {
propsData: {
mr: {
mergeError: 'Merge error happened',
},
...props,
},
- data() {
- return data;
- },
});
};
@@ -121,7 +118,9 @@ describe('MRWidgetFailedToMerge', () => {
describe('while it is refreshing', () => {
it('renders Refresing now', async () => {
- createComponent({}, { isRefreshing: true });
+ createComponent({});
+
+ wrapper.vm.refresh();
await nextTick();
@@ -138,8 +137,10 @@ describe('MRWidgetFailedToMerge', () => {
createComponent();
});
- it('renders warning icon and disabled merge button', () => {
- expect(wrapper.find('.js-ci-status-icon-warning')).not.toBeNull();
+ it('renders failed icon', () => {
+ createComponent({}, mount);
+
+ expect(wrapper.find('[data-testid="status-failed-icon"]').exists()).toBe(true);
});
it('renders given error', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 07fc0be9e51..48b86d879ad 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -58,7 +58,7 @@ const createTestMr = (customConfig) => {
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition),
translateStateToMachine: () => this.transitionStateMachine(),
- state: 'open',
+ state: 'readyToMerge',
canMerge: true,
mergeable: true,
userPermissions: {
@@ -113,11 +113,6 @@ const createComponent = (customConfig = {}, createState = true) => {
GlSprintf,
},
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
- provide: {
- glFeatures: {
- autoMergeLabelsMrWidget: false,
- },
- },
});
};
@@ -144,6 +139,7 @@ const findDeleteSourceBranchCheckbox = () =>
const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
const triggerEditCommitInput = () =>
wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+const findMergeHelperText = () => wrapper.find('[data-testid="auto-merge-helper-text"]');
describe('ReadyToMerge', () => {
beforeEach(() => {
@@ -185,47 +181,22 @@ describe('ReadyToMerge', () => {
expect(wrapper.vm.status).toEqual('failed');
});
});
-
- describe('status icon', () => {
- it('defaults to tick icon', () => {
- createComponent({ mr: { mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
-
- it('shows tick for success status', () => {
- createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
-
- it('shows tick for pending status', () => {
- createComponent({ mr: { pipeline: { active: true }, mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
- });
});
describe('merge button text', () => {
it('should return "Merge" when no auto merge strategies are available', () => {
- createComponent({ mr: { availableAutoMergeStrategies: [] } });
-
- expect(findMergeButton().text()).toBe('Merge');
- });
-
- it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
createComponent({
- mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
+ mr: { availableAutoMergeStrategies: [] },
});
- expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ expect(findMergeButton().text()).toBe('Merge');
});
- it('should return Merge when pipeline succeeds', () => {
+ it('should return Set to auto-merge in the button and Merge when pipeline succeeds in the helper text', () => {
createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
- expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ expect(findMergeButton().text()).toBe('Set to auto-merge');
+ expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds');
});
});
@@ -258,10 +229,10 @@ describe('ReadyToMerge', () => {
expect(findMergeButton().props('disabled')).toBe(true);
});
- it('should be disabled if merge is not allowed', () => {
- createComponent({ mr: { preventMerge: true } });
+ it('should not exist if merge is not allowed', () => {
+ createComponent({ mr: { state: 'checking' } });
- expect(findMergeButton().props('disabled')).toBe(true);
+ expect(findMergeButton().exists()).toBe(false);
});
it('should be disabled when making request', async () => {
@@ -321,7 +292,7 @@ describe('ReadyToMerge', () => {
describe('Merge Button Variant', () => {
it('defaults to confirm class', () => {
createComponent({
- mr: { availableAutoMergeStrategies: [], mergeable: true },
+ mr: { availableAutoMergeStrategies: [] },
});
expect(findMergeButton().attributes('variant')).toBe('confirm');
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index 296d7924243..02d17b8dfd2 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -16,14 +16,16 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
<div class=\\"gl-display-flex gl-align-items-baseline\\">
<status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub>
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">Main text for the row</p>
- <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
- <!---->
- <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
- Badge is optional. Text to be displayed inside badge
- </gl-badge-stub>
+ <div class=\\"gl-w-full gl-display-flex\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\">
+ <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p>
+ <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
+ <!---->
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
+ Badge is optional. Text to be displayed inside badge
+ </gl-badge-stub>
+ </div>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
<p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
</div>
@@ -40,12 +42,14 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
<div class=\\"gl-display-flex gl-align-items-baseline\\">
<!---->
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
- <!---->
- <!---->
- <!---->
+ <div class=\\"gl-w-full gl-display-flex\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\">
+ <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p>
+ <!---->
+ <!---->
+ <!---->
+ </div>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
<!---->
</div>
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 4972c522733..9343a3a5e90 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
@@ -9,6 +9,7 @@ import ActionButtons from '~/vue_merge_request_widget/components/widget/action_b
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
import * as logger from '~/lib/logger';
+import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({
@@ -29,7 +30,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller');
- const createComponent = ({ propsData, slots, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = async ({ propsData, slots, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(Widget, {
propsData: {
isCollapsible: false,
@@ -49,6 +50,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
ContentRow: WidgetContentRow,
},
});
+
+ await axios.waitForAll();
};
describe('on mount', () => {
@@ -105,9 +108,9 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
},
});
- expect(wrapper.text()).not.toContain('Loading');
- await nextTick();
expect(wrapper.text()).toContain('Loading');
+ await axios.waitForAll();
+ expect(wrapper.text()).not.toContain('Loading');
});
it('validates widget name', () => {
@@ -185,10 +188,10 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
describe('content', () => {
- it('displays summary property when summary slot is not provided', () => {
- createComponent({
+ it('displays summary property when summary slot is not provided', async () => {
+ await createComponent({
propsData: {
- summary: 'Hello world',
+ summary: { title: 'Hello world' },
},
});
@@ -256,8 +259,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
describe('handle collapse toggle', () => {
- it('displays the toggle button correctly', () => {
- createComponent({
+ it('displays the toggle button correctly', async () => {
+ await createComponent({
propsData: {
isCollapsible: true,
},
@@ -271,7 +274,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
it('does not display the content slot until toggle is clicked', async () => {
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
},
@@ -286,8 +289,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(findExpandedSection().text()).toBe('More complex content');
});
- it('emits a toggle even when button is toggled', () => {
- createComponent({
+ it('emits a toggle even when button is toggled', async () => {
+ await createComponent({
propsData: {
isCollapsible: true,
},
@@ -301,8 +304,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(wrapper.emitted('toggle')).toEqual([[{ expanded: true }]]);
});
- it('does not display the toggle button if isCollapsible is false', () => {
- createComponent({
+ it('does not display the toggle button if isCollapsible is false', async () => {
+ await createComponent({
propsData: {
isCollapsible: false,
},
@@ -326,7 +329,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded);
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
@@ -358,7 +361,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('allows refetching when fetch expanded data returns an error', async () => {
const fetchExpandedData = jest.fn().mockRejectedValue({ error: true });
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
fetchExpandedData,
@@ -385,7 +388,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('resets the error message when another request is fetched', async () => {
const fetchExpandedData = jest.fn().mockRejectedValue({ error: true });
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
fetchExpandedData,
@@ -465,8 +468,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
},
];
- beforeEach(() => {
- createComponent({
+ beforeEach(async () => {
+ await createComponent({
mountFn: mountExtended,
propsData: {
isCollapsible: true,
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 5baed8ff211..6aa12c37374 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
@@ -5,9 +5,7 @@ import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
-import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import terraformExtension from '~/vue_merge_request_widget/extensions/terraform';
+import terraformExtension from '~/vue_merge_request_widget/extensions/terraform/index.vue';
import {
plans,
validPlanWithName,
@@ -25,22 +23,20 @@ describe('Terraform extension', () => {
const endpoint = '/path/to/terraform/report.json';
const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
-
- registerExtension(terraformExtension);
+ const findActionButton = (at) => wrapper.findAllByTestId('extension-actions-button').at(at);
const mockPollingApi = (response, body, header) => {
mock.onGet(endpoint).reply(response, body, header);
};
const createComponent = () => {
- wrapper = mountExtended(extensionsContainer, {
+ wrapper = mountExtended(terraformExtension, {
propsData: {
mr: {
terraformReportsPath: endpoint,
},
},
});
- return axios.waitForAll();
};
beforeEach(() => {
@@ -54,24 +50,27 @@ describe('Terraform extension', () => {
describe('summary', () => {
describe('while loading', () => {
const loadingText = 'Loading Terraform reports...';
+
it('should render loading text', async () => {
mockPollingApi(HTTP_STATUS_OK, plans, {});
createComponent();
expect(wrapper.text()).toContain(loadingText);
+
await waitForPromises();
expect(wrapper.text()).not.toContain(loadingText);
});
});
describe('when the fetching fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
- it('should generate one invalid plan and render correct summary text', () => {
- expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ it('should show the error text', () => {
+ expect(wrapper.text()).toContain('Failed to load Terraform reports');
});
});
@@ -82,9 +81,10 @@ describe('Terraform extension', () => {
${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''}
${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
`('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_OK, response, {});
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
it(`should render correct summary text`, () => {
@@ -101,7 +101,8 @@ describe('Terraform extension', () => {
describe('expanded data', () => {
beforeEach(async () => {
mockPollingApi(HTTP_STATUS_OK, plans, {});
- await createComponent();
+ createComponent();
+ await axios.waitForAll();
wrapper.findByTestId('toggle-button').trigger('click');
});
@@ -136,7 +137,7 @@ describe('Terraform extension', () => {
api.trackRedisHllUserEvent.mockClear();
api.trackRedisCounterEvent.mockClear();
- findListItem(0).find('[data-testid="extension-actions-button"]').trigger('click');
+ findActionButton(0).trigger('click');
expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
@@ -161,10 +162,10 @@ describe('Terraform extension', () => {
});
describe('successful poll', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_OK, plans, {});
-
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
it('does not make additional requests after poll is successful', () => {
@@ -173,13 +174,14 @@ describe('Terraform extension', () => {
});
describe('polling fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
- it('generates one broken plan', () => {
- expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ it('renders the error text', () => {
+ expect(wrapper.text()).toContain('Failed to load Terraform reports');
});
it('does not make additional requests after poll is unsuccessful', () => {
diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 47143bb2bb8..9da687c0ff8 100644
--- a/spec/frontend/vue_merge_request_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
@@ -188,7 +188,11 @@ export default {
coverage: '92.16',
path: '/root/acets-app/pipelines/172',
details: {
- artifacts,
+ artifacts: artifacts.map(({ text, url, ...rest }) => ({
+ name: text,
+ path: url,
+ ...rest,
+ })),
status: {
icon: 'status_success',
favicon: 'favicon_status_success',
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 0533471bece..ecb5a8448f9 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,5 +1,4 @@
import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -10,6 +9,7 @@ import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_stat
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
@@ -28,6 +28,8 @@ import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
+import WidgetSuggestPipeline from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
+import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import getStateSubscription from '~/vue_merge_request_widget/queries/get_state.subscription.graphql';
@@ -76,6 +78,9 @@ describe('MrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
const findApprovalsWidget = () => wrapper.findComponent(Approvals);
const findPreparingWidget = () => wrapper.findComponent(Preparing);
+ const findMergedPipelineContainer = () => wrapper.findByTestId('merged-pipeline-container');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findAlertMessage = () => wrapper.findComponent(MrWidgetAlertMessage);
beforeEach(() => {
gl.mrWidgetData = { ...mockData };
@@ -95,7 +100,12 @@ describe('MrWidgetOptions', () => {
gl.mrWidgetData = {};
});
- const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
+ const createComponent = ({
+ mrData = mockData,
+ options = {},
+ data = {},
+ mountFn = shallowMountExtended,
+ } = {}) => {
const mockedApprovalsSubscription = createMockApolloSubscription();
queryResponse = {
data: {
@@ -114,7 +124,6 @@ describe('MrWidgetOptions', () => {
stateQueryHandler = jest.fn().mockResolvedValue(queryResponse);
stateSubscription = createMockApolloSubscription();
- const mounting = fullMount ? mount : shallowMount;
const queryHandlers = [
[approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)],
[getStateQuery, stateQueryHandler],
@@ -143,7 +152,7 @@ describe('MrWidgetOptions', () => {
apolloProvider.defaultClient.setRequestHandler(query, stream);
});
- wrapper = mounting(MrWidgetOptions, {
+ wrapper = mountFn(MrWidgetOptions, {
propsData: {
mrData: { ...mrData },
},
@@ -165,8 +174,7 @@ describe('MrWidgetOptions', () => {
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
const findExtensionLink = (linkHref) =>
wrapper.find(`[data-testid="widget-extension"] [href="${linkHref}"]`);
- const findSuggestPipeline = () => wrapper.find('[data-testid="mr-suggest-pipeline"]');
- const findSuggestPipelineButton = () => findSuggestPipeline().find('button');
+ const findSuggestPipeline = () => wrapper.findComponent(WidgetSuggestPipeline);
const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
describe('default', () => {
@@ -175,7 +183,7 @@ describe('MrWidgetOptions', () => {
return createComponent();
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/385238
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/385238
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('data', () => {
it('should instantiate Store and Service', () => {
@@ -186,6 +194,7 @@ describe('MrWidgetOptions', () => {
describe('computed', () => {
describe('componentName', () => {
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/409365
// eslint-disable-next-line jest/no-disabled-tests
it.skip.each`
${'merged'} | ${'mr-widget-merged'}
@@ -206,60 +215,18 @@ describe('MrWidgetOptions', () => {
});
});
- describe('shouldRenderPipelines', () => {
- it('should return true when hasCI is true', () => {
+ describe('MrWidgetPipelineContainer', () => {
+ it('should return true when hasCI is true', async () => {
wrapper.vm.mr.hasCI = true;
-
- expect(wrapper.vm.shouldRenderPipelines).toBe(true);
+ await nextTick();
+ expect(findPipelineContainer().exists()).toBe(true);
});
- it('should return false when hasCI is false', () => {
+ it('should return false when hasCI is false', async () => {
wrapper.vm.mr.hasCI = false;
+ await nextTick();
- expect(wrapper.vm.shouldRenderPipelines).toBe(false);
- });
- });
-
- describe('shouldRenderSourceBranchRemovalStatus', () => {
- beforeEach(() => {
- wrapper.vm.mr.state = 'readyToMerge';
- });
-
- it('should return true when cannot remove source branch and branch will be removed', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(true);
- });
-
- it('should return false when can remove source branch and branch will be removed', () => {
- wrapper.vm.mr.canRemoveSourceBranch = true;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
- });
-
- it('should return false when cannot remove source branch and branch will not be removed', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = false;
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
- });
-
- it('should return false when in merged state', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
- wrapper.vm.mr.state = 'merged';
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
- });
-
- it('should return false when in nothing to merge state', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
- wrapper.vm.mr.state = 'nothingToMerge';
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
+ expect(findPipelineContainer().exists()).toBe(false);
});
});
@@ -320,7 +287,7 @@ describe('MrWidgetOptions', () => {
});
it('should be false', () => {
- expect(wrapper.vm.showMergePipelineForkWarning).toEqual(false);
+ expect(findAlertMessage().exists()).toBe(false);
});
});
@@ -333,7 +300,7 @@ describe('MrWidgetOptions', () => {
});
it('should be false', () => {
- expect(wrapper.vm.showMergePipelineForkWarning).toEqual(false);
+ expect(findAlertMessage().exists()).toBe(false);
});
});
@@ -346,22 +313,30 @@ describe('MrWidgetOptions', () => {
});
it('should be true', () => {
- expect(wrapper.vm.showMergePipelineForkWarning).toEqual(true);
+ expect(findAlertMessage().exists()).toBe(true);
});
});
});
describe('formattedHumanAccess', () => {
- it('when user is a tool admin but not a member of project', () => {
+ it('when user is a tool admin but not a member of project', async () => {
wrapper.vm.mr.humanAccess = null;
+ wrapper.vm.mr.mergeRequestAddCiConfigPath = 'test';
+ wrapper.vm.mr.hasCI = false;
+ wrapper.vm.mr.isDismissedSuggestPipeline = false;
+ await nextTick();
- expect(wrapper.vm.formattedHumanAccess).toEqual('');
+ expect(findSuggestPipeline().props('humanAccess')).toBe('');
});
- it('when user a member of the project', () => {
+ it('when user a member of the project', async () => {
wrapper.vm.mr.humanAccess = 'Owner';
+ wrapper.vm.mr.mergeRequestAddCiConfigPath = 'test';
+ wrapper.vm.mr.hasCI = false;
+ wrapper.vm.mr.isDismissedSuggestPipeline = false;
+ await nextTick();
- expect(wrapper.vm.formattedHumanAccess).toEqual('owner');
+ expect(findSuggestPipeline().props('humanAccess')).toBe('owner');
});
});
});
@@ -570,10 +545,10 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
wrapper.destroy();
- return createComponent(
- mockData,
- {},
- {
+ return createComponent({
+ mrData: mockData,
+ options: {},
+ data: {
pollInterval: interval,
startingPollInterval: interval,
mr: {
@@ -584,8 +559,7 @@ describe('MrWidgetOptions', () => {
checkStatus: mockCheckStatus,
},
},
- false,
- );
+ });
});
describe('normal polling behavior', () => {
@@ -653,7 +627,7 @@ describe('MrWidgetOptions', () => {
environment_available: true,
};
- beforeEach(() => {
+ it('renders multiple deployments', async () => {
wrapper.vm.mr.deployments.push(
{
...deploymentMockData,
@@ -663,19 +637,10 @@ describe('MrWidgetOptions', () => {
id: deploymentMockData.id + 1,
},
);
-
- return nextTick();
- });
-
- it('renders multiple deployments', () => {
- expect(wrapper.findAll('.deploy-heading').length).toBe(2);
- });
-
- it('renders dropdpown with multiple file changes', () => {
- expect(
- wrapper.find('.js-mr-wigdet-deployment-dropdown').findAll('.js-filtered-dropdown-result')
- .length,
- ).toEqual(changes.length);
+ await nextTick();
+ expect(findPipelineContainer().props('isPostMerge')).toBe(false);
+ expect(findPipelineContainer().props('mr').deployments).toHaveLength(2);
+ expect(findPipelineContainer().props('mr').postMergeDeployments).toHaveLength(0);
});
});
@@ -793,7 +758,7 @@ describe('MrWidgetOptions', () => {
});
it('renders pipeline block', () => {
- expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(true);
+ expect(findMergedPipelineContainer().exists()).toBe(true);
});
describe('with post merge deployments', () => {
@@ -833,7 +798,7 @@ describe('MrWidgetOptions', () => {
});
it('renders post deployment information', () => {
- expect(wrapper.find('.js-post-deployment').exists()).toBe(true);
+ expect(findMergedPipelineContainer().exists()).toBe(true);
});
});
});
@@ -846,7 +811,7 @@ describe('MrWidgetOptions', () => {
});
it('does not render pipeline block', () => {
- expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(false);
+ expect(findMergedPipelineContainer().exists()).toBe(false);
});
});
@@ -858,11 +823,7 @@ describe('MrWidgetOptions', () => {
});
it('does not render pipeline block', () => {
- expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(false);
- });
-
- it('does not render post deployment information', () => {
- expect(wrapper.find('.js-post-deployment').exists()).toBe(false);
+ expect(findMergedPipelineContainer().exists()).toBe(false);
});
});
});
@@ -880,7 +841,6 @@ describe('MrWidgetOptions', () => {
describe('given feature flag is enabled', () => {
beforeEach(async () => {
await createComponent();
-
wrapper.vm.mr.hasCI = false;
});
@@ -901,7 +861,7 @@ describe('MrWidgetOptions', () => {
});
it('should allow dismiss of the suggest pipeline message', async () => {
- await findSuggestPipelineButton().trigger('click');
+ await findSuggestPipeline().vm.$emit('dismiss');
expect(findSuggestPipeline().exists()).toBe(false);
});
@@ -915,7 +875,7 @@ describe('MrWidgetOptions', () => {
${'merged'} | ${true} | ${'shows'}
${'open'} | ${true} | ${'shows'}
`('$showText merge error when state is $state', async ({ state, show }) => {
- createComponent({ ...mockData, state, mergeError: 'Error!' });
+ createComponent({ mrData: { ...mockData, state, mergeError: 'Error!' } });
await waitForPromises();
@@ -927,7 +887,7 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
registerExtension(workingExtension());
- createComponent();
+ createComponent({ mountFn: mountExtended });
});
afterEach(() => {
@@ -987,7 +947,7 @@ describe('MrWidgetOptions', () => {
it('shows collapse button', async () => {
registerExtension(workingExtension(true));
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findExtensionToggleButton().exists()).toBe(true);
});
@@ -1026,7 +986,7 @@ describe('MrWidgetOptions', () => {
]),
);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findWidgetTestExtension().html()).toContain(
'Multi polling test extension reports: parsed, count: 2',
);
@@ -1048,7 +1008,7 @@ describe('MrWidgetOptions', () => {
]),
);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findWidgetTestExtension().html()).toContain('Test extension loading...');
});
});
@@ -1057,7 +1017,7 @@ describe('MrWidgetOptions', () => {
it('does not make additional requests after poll is successful', async () => {
registerExtension(pollingExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(pollRequest).toHaveBeenCalledTimes(1);
});
@@ -1067,7 +1027,7 @@ describe('MrWidgetOptions', () => {
it('sets data when polling is complete', async () => {
registerExtension(pollingFullDataExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
api.trackRedisHllUserEvent.mockClear();
api.trackRedisCounterEvent.mockClear();
@@ -1095,14 +1055,14 @@ describe('MrWidgetOptions', () => {
describe('error', () => {
it('does not make additional requests after poll has failed', async () => {
registerExtension(pollingErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(pollRequest).toHaveBeenCalledTimes(1);
});
it('captures sentry error and displays error when poll has failed', async () => {
registerExtension(pollingErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(Sentry.captureException).toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error'));
@@ -1118,7 +1078,7 @@ describe('MrWidgetOptions', () => {
it('handles collapsed data fetch errors', async () => {
registerExtension(collapsedDataErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]').exists(),
@@ -1130,7 +1090,7 @@ describe('MrWidgetOptions', () => {
it('handles full data fetch errors', async () => {
registerExtension(fullDataErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(wrapper.findComponent(StatusIcon).props('iconName')).not.toBe('error');
wrapper
@@ -1153,7 +1113,7 @@ describe('MrWidgetOptions', () => {
it('triggers view events when mounted', () => {
registerExtension(workingExtension());
- createComponent();
+ createComponent({ mountFn: mountExtended });
expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
@@ -1168,7 +1128,7 @@ describe('MrWidgetOptions', () => {
describe('expand button', () => {
it('triggers expand events when clicked', async () => {
registerExtension(workingExtension());
- createComponent();
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -1197,7 +1157,7 @@ describe('MrWidgetOptions', () => {
it('triggers the "full report clicked" events when the appropriate button is clicked', () => {
registerExtension(fullReportExtension);
- createComponent();
+ createComponent({ mountFn: mountExtended });
api.trackRedisHllUserEvent.mockClear();
api.trackRedisCounterEvent.mockClear();
@@ -1221,7 +1181,7 @@ describe('MrWidgetOptions', () => {
it("doesn't emit any telemetry events", async () => {
registerExtension(noTelemetryExtension);
- createComponent();
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -1249,7 +1209,7 @@ describe('MrWidgetOptions', () => {
});
it('does not render the Preparing state component by default', async () => {
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findApprovalsWidget().exists()).toBe(true);
expect(findPreparingWidget().exists()).toBe(false);
@@ -1257,9 +1217,11 @@ describe('MrWidgetOptions', () => {
it('renders the Preparing state component when the MR state is initially "preparing"', async () => {
await createComponent({
- ...mockData,
- state: 'opened',
- detailedMergeStatus: 'PREPARING',
+ mrData: {
+ ...mockData,
+ state: 'opened',
+ detailedMergeStatus: 'PREPARING',
+ },
});
expect(findApprovalsWidget().exists()).toBe(false);
@@ -1272,31 +1234,29 @@ describe('MrWidgetOptions', () => {
});
it("shows the Preparing widget when the MR reports it's not ready yet", async () => {
- await createComponent(
- {
+ await createComponent({
+ mrData: {
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
},
- {},
- {},
- false,
- );
+ options: {},
+ data: {},
+ });
expect(wrapper.html()).toContain('mr-widget-preparing-stub');
});
it('removes the Preparing widget when the MR indicates it has been prepared', async () => {
- await createComponent(
- {
+ await createComponent({
+ mrData: {
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
},
- {},
- {},
- false,
- );
+ options: {},
+ data: {},
+ });
expect(wrapper.html()).toContain('mr-widget-preparing-stub');
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 217103ab25c..cfd0d5bcf89 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
@@ -1,5 +1,7 @@
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue';
import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql';
@@ -9,41 +11,39 @@ const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar To Do', () => {
let wrapper;
+ let requestHandler;
- function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
+ const defaultHandler = {
+ createAlertTodo: jest.fn().mockResolvedValue({}),
+ markAsDone: jest.fn().mockResolvedValue({}),
+ };
+
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ requestHandler = handler;
+
+ return createMockApollo([
+ [todoMarkDoneMutation, handler.markAsDone],
+ [createAlertTodoMutation, handler.createAlertTodo],
+ ]);
+ };
+
+ function mountComponent({ data, sidebarCollapsed = true, handler = defaultHandler } = {}) {
wrapper = mount(SidebarTodo, {
+ apolloProvider: createMockApolloProvider(handler),
propsData: {
alert: { ...mockAlert },
...data,
sidebarCollapsed,
projectPath: 'projectPath',
},
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- queries: {
- alert: {
- loading,
- },
- },
- },
- },
- stubs,
});
}
const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]');
describe('updating the alert to do', () => {
- const mockUpdatedMutationResult = {
- data: {
- updateAlertTodo: {
- errors: [],
- alert: {},
- },
- },
- };
-
describe('adding a todo', () => {
beforeEach(() => {
mountComponent({
@@ -60,18 +60,15 @@ describe('Alert Details Sidebar To Do', () => {
});
it('calls `$apollo.mutate` with `createAlertTodoMutation` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
-
findToDoButton().trigger('click');
await nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: createAlertTodoMutation,
- variables: {
+ expect(requestHandler.createAlertTodo).toHaveBeenCalledWith(
+ expect.objectContaining({
iid: '1527542',
projectPath: 'projectPath',
- },
- });
+ }),
+ );
});
});
@@ -91,17 +88,11 @@ describe('Alert Details Sidebar To Do', () => {
});
it('calls `$apollo.mutate` with `todoMarkDoneMutation` mutation and variables containing `id`', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
-
findToDoButton().trigger('click');
await nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: todoMarkDoneMutation,
- update: expect.anything(),
- variables: {
- id: '1234',
- },
+ expect(requestHandler.markAsDone).toHaveBeenCalledWith({
+ id: '1234',
});
});
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 98cb2f5cb0b..90d29f0bfd4 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,7 +1,9 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
import Tracking from '~/tracking';
import AlertManagementStatus from '~/vue_shared/alert_details/components/alert_status.vue';
@@ -11,6 +13,27 @@ const mockAlert = mockAlerts[0];
describe('AlertManagementStatus', () => {
let wrapper;
+ let requestHandler;
+
+ const iid = '1527542';
+ const mockUpdatedMutationResult = ({ errors = [], nodes = [] } = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ updateAlertStatus: {
+ errors,
+ alert: {
+ id: '1',
+ iid,
+ status: 'acknowledged',
+ endedAt: 'endedAt',
+ notes: {
+ nodes,
+ },
+ },
+ },
+ },
+ });
+
const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
const findFirstStatusOption = () => findStatusDropdown().findComponent(GlDropdownItem);
const findAllStatusOptions = () => findStatusDropdown().findAllComponents(GlDropdownItem);
@@ -22,8 +45,20 @@ describe('AlertManagementStatus', () => {
return waitForPromises();
};
- function mountComponent({ props = {}, provide = {}, loading = false, stubs = {} } = {}) {
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+ requestHandler = handler;
+
+ return createMockApollo([[updateAlertStatusMutation, handler]]);
+ };
+
+ function mountComponent({
+ props = {},
+ provide = {},
+ handler = mockUpdatedMutationResult(),
+ } = {}) {
wrapper = shallowMountExtended(AlertManagementStatus, {
+ apolloProvider: createMockApolloProvider(handler),
propsData: {
alert: { ...mockAlert },
projectPath: 'gitlab-org/gitlab',
@@ -31,17 +66,6 @@ describe('AlertManagementStatus', () => {
...props,
},
provide,
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- queries: {
- alert: {
- loading,
- },
- },
- },
- },
- stubs,
});
}
@@ -63,43 +87,32 @@ describe('AlertManagementStatus', () => {
});
describe('updating the alert status', () => {
- const iid = '1527542';
- const mockUpdatedMutationResult = {
- data: {
- updateAlertStatus: {
- errors: [],
- alert: {
- iid,
- status: 'acknowledged',
- },
- },
- },
- };
-
beforeEach(() => {
- mountComponent({});
+ mountComponent();
});
- it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', async () => {
findFirstStatusOption().vm.$emit('click');
+ await waitForPromises();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateAlertStatusMutation,
- variables: {
- iid,
- status: 'TRIGGERED',
- projectPath: 'gitlab-org/gitlab',
- },
+ expect(requestHandler).toHaveBeenCalledWith({
+ iid,
+ status: 'TRIGGERED',
+ projectPath: 'gitlab-org/gitlab',
});
});
describe('when a request fails', () => {
- beforeEach(() => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
+ beforeEach(async () => {
+ mountComponent({
+ handler: mockUpdatedMutationResult({ errors: ['<span data-testid="htmlError" />'] }),
+ });
+ await waitForPromises();
});
it('emits an error', async () => {
+ mountComponent({ handler: jest.fn().mockRejectedValue({}) });
+ await waitForPromises();
await selectFirstStatusOption();
expect(wrapper.emitted('alert-error')[0]).toEqual([
@@ -116,7 +129,6 @@ describe('AlertManagementStatus', () => {
it('emits an error when triggered a second time', async () => {
await selectFirstStatusOption();
- await nextTick();
await selectFirstStatusOption();
// Should emit two errors [0,1]
expect(wrapper.emitted('alert-error').length > 1).toBe(true);
@@ -124,19 +136,9 @@ describe('AlertManagementStatus', () => {
});
it('shows an error when response includes HTML errors', async () => {
- const mockUpdatedMutationErrorResult = {
- data: {
- updateAlertStatus: {
- errors: ['<span data-testid="htmlError" />'],
- alert: {
- iid,
- status: 'acknowledged',
- },
- },
- },
- };
-
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult);
+ mountComponent({
+ handler: mockUpdatedMutationResult({ errors: ['<span data-testid="htmlError" />'] }),
+ });
await selectFirstStatusOption();
@@ -160,7 +162,7 @@ describe('AlertManagementStatus', () => {
mountComponent({
props: { alert: { ...mockAlert, status }, statuses: { [status]: translatedStatus } },
});
- expect(findAllStatusOptions().length).toBe(1);
+ expect(findAllStatusOptions()).toHaveLength(1);
expect(findFirstStatusOption().text()).toBe(translatedStatus);
});
});
@@ -173,10 +175,10 @@ describe('AlertManagementStatus', () => {
it('should not track alert status updates when the tracking options do not exist', async () => {
mountComponent({});
Tracking.event.mockClear();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
+
findFirstStatusOption().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(Tracking.event).not.toHaveBeenCalled();
});
@@ -187,12 +189,14 @@ describe('AlertManagementStatus', () => {
action: 'update_alert_status',
label: 'Status',
};
- mountComponent({ provide: { trackAlertStatusUpdateOptions } });
+ mountComponent({
+ provide: { trackAlertStatusUpdateOptions },
+ handler: mockUpdatedMutationResult({ nodes: mockAlerts }),
+ });
Tracking.event.mockClear();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
const status = findFirstStatusOption().text();
const { category, action, label } = trackAlertStatusUpdateOptions;
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index e7663e2adb2..9f9a27c6997 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -31,12 +31,13 @@ const TEST_ACTION_2 = {
describe('vue_shared/components/actions_button', () => {
let wrapper;
- function createComponent(props) {
+ function createComponent({ props = {}, slots = {} } = {}) {
wrapper = shallowMountExtended(ActionsButton, {
propsData: { actions: [TEST_ACTION, TEST_ACTION_2], toggleText: 'Edit', ...props },
stubs: {
GlDisclosureDropdownItem,
},
+ slots,
});
}
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
@@ -47,11 +48,29 @@ describe('vue_shared/components/actions_button', () => {
expect(findDropdown().props().toggleText).toBe('Edit');
});
+ it('dropdown has a fluid width', () => {
+ createComponent();
+
+ expect(findDropdown().props().fluidWidth).toBe(true);
+ });
+
+ it('provides a default slot', () => {
+ const slotContent = 'default text';
+
+ createComponent({
+ slots: {
+ default: slotContent,
+ },
+ });
+
+ expect(findDropdown().text()).toContain(slotContent);
+ });
+
it('allows customizing variant and category', () => {
const variant = 'confirm';
const category = 'secondary';
- createComponent({ variant, category });
+ createComponent({ props: { variant, category } });
expect(findDropdown().props()).toMatchObject({ category, variant });
});
@@ -88,4 +107,13 @@ describe('vue_shared/components/actions_button', () => {
});
});
});
+
+ it.each(['shown', 'hidden'])(
+ 'bubbles up %s event from the disclosure dropdown component',
+ (event) => {
+ createComponent();
+ findDropdown().vm.$emit(event);
+ expect(wrapper.emitted(event)).toHaveLength(1);
+ },
+ );
});
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index da5516f8db1..6c28347503c 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -74,6 +74,7 @@ describe('vue_shared/components/awards_list', () => {
return {
classes: x.classes(),
title: x.attributes('title'),
+ emojiName: x.attributes('data-emoji-name'),
html: x.find('[data-testid="award-html"]').html(),
count: Number(x.find('.js-counter').text()),
};
@@ -96,48 +97,56 @@ describe('vue_shared/components/awards_list', () => {
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
+ emojiName: EMOJI_THUMBSUP,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: `You, Ada, and Marie reacted with :${EMOJI_THUMBSDOWN}:`,
+ emojiName: EMOJI_THUMBSDOWN,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: `Ada reacted with :${EMOJI_100}:`,
+ emojiName: EMOJI_100,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
+ emojiName: EMOJI_SMILE,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 4,
html: matchingEmojiTag(EMOJI_OK),
title: `You, Ada, Jane, and Leonardo reacted with :${EMOJI_OK}:`,
+ emojiName: EMOJI_OK,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_CACTUS),
title: `You reacted with :${EMOJI_CACTUS}:`,
+ emojiName: EMOJI_CACTUS,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_A),
title: `Marie reacted with :${EMOJI_A}:`,
+ emojiName: EMOJI_A,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_B),
title: `You reacted with :${EMOJI_B}:`,
+ emojiName: EMOJI_B,
},
]);
});
@@ -226,12 +235,14 @@ describe('vue_shared/components/awards_list', () => {
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: '',
+ emojiName: EMOJI_THUMBSUP,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: '',
+ emojiName: EMOJI_THUMBSDOWN,
},
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
{
@@ -239,12 +250,14 @@ describe('vue_shared/components/awards_list', () => {
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: `Marie reacted with :${EMOJI_100}:`,
+ emojiName: EMOJI_100,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_SMILE),
title: `Marie reacted with :${EMOJI_SMILE}:`,
+ emojiName: EMOJI_SMILE,
},
]);
});
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 6acd1f51a86..1f3029435ee 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
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { handleBlobRichViewer } from '~/blob/viewer';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
@@ -21,16 +22,24 @@ describe('Blob Rich Viewer component', () => {
}
beforeEach(() => {
+ const execImmediately = (callback) => callback();
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+
createComponent();
});
+ it('listens to requestIdleCallback', () => {
+ expect(window.requestIdleCallback).toHaveBeenCalled();
+ });
+
it('renders the passed content without transformations', () => {
expect(wrapper.html()).toContain(content);
});
- it('renders the richViewer if one is present', () => {
+ it('renders the richViewer if one is present', async () => {
const richViewer = '<div class="js-pdf-viewer"></div>';
createComponent('pdf', richViewer);
+ await nextTick();
expect(wrapper.html()).toContain(richViewer);
});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 31d63654168..c907b776b91 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -1,18 +1,23 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => {
let wrapper;
- const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]');
+ const createComponent = (props) => {
+ wrapper = shallowMount(CiIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
it('should render a span element with an svg', () => {
- wrapper = shallowMount(ciIcon, {
- propsData: {
- status: {
- icon: 'status_success',
- },
+ createComponent({
+ status: {
+ group: 'success',
+ icon: 'status_success',
},
});
@@ -20,49 +25,43 @@ describe('CI Icon component', () => {
expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
- describe('active icons', () => {
- it.each`
- isActive | cssClass
- ${true} | ${'active'}
- ${false} | ${'active'}
- `('active should be $isActive', ({ isActive, cssClass }) => {
- wrapper = shallowMount(ciIcon, {
+ describe.each`
+ isActive
+ ${true}
+ ${false}
+ `('when isActive is $isActive', ({ isActive }) => {
+ it(`"active" class is ${isActive ? 'not ' : ''}added`, () => {
+ wrapper = shallowMount(CiIcon, {
propsData: {
status: {
+ group: 'success',
icon: 'status_success',
},
isActive,
},
});
- if (isActive) {
- expect(findIconWrapper().classes()).toContain(cssClass);
- } else {
- expect(findIconWrapper().classes()).not.toContain(cssClass);
- }
+ expect(wrapper.classes('active')).toBe(isActive);
});
});
- describe('interactive icons', () => {
- it.each`
- isInteractive | cssClass
- ${true} | ${'interactive'}
- ${false} | ${'interactive'}
- `('interactive should be $isInteractive', ({ isInteractive, cssClass }) => {
- wrapper = shallowMount(ciIcon, {
+ describe.each`
+ isInteractive
+ ${true}
+ ${false}
+ `('when isInteractive is $isInteractive', ({ isInteractive }) => {
+ it(`"interactive" class is ${isInteractive ? 'not ' : ''}added`, () => {
+ wrapper = shallowMount(CiIcon, {
propsData: {
status: {
+ group: 'success',
icon: 'status_success',
},
isInteractive,
},
});
- if (isInteractive) {
- expect(findIconWrapper().classes()).toContain(cssClass);
- } else {
- expect(findIconWrapper().classes()).not.toContain(cssClass);
- }
+ expect(wrapper.classes('interactive')).toBe(isInteractive);
});
});
@@ -79,7 +78,7 @@ describe('CI Icon component', () => {
${'status_canceled'} | ${'canceled'} | ${'ci-status-icon-canceled'}
${'status_manual'} | ${'manual'} | ${'ci-status-icon-manual'}
`('should render a $group status', ({ icon, group, cssClass }) => {
- wrapper = shallowMount(ciIcon, {
+ wrapper = shallowMount(CiIcon, {
propsData: {
status: {
icon,
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 25283eb1211..5720f45f4dd 100644
--- a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -58,4 +58,11 @@ describe('Code Block Highlighted', () => {
</code-block-stub>
`);
});
+
+ it('renders content as plain text language is not supported', () => {
+ const content = '<script>alert("xss")</script>';
+ createComponent({ code: content, language: 'foobar' });
+
+ expect(wrapper.text()).toContain(content);
+ });
});
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 d7f94c00d09..0b5c8d9afc3 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
@@ -60,9 +60,7 @@ describe('Confirm Danger Modal', () => {
});
it('renders the correct confirmation phrase', () => {
- expect(findConfirmationPhrase().text()).toBe(
- `Please type ${phrase} to proceed or close this modal to cancel.`,
- );
+ expect(findConfirmationPhrase().text()).toBe(`Please type ${phrase} to proceed.`);
});
describe('without injected data', () => {
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 2a4037d76b7..40232eb367a 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -8,6 +8,7 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
jest.mock('fuzzaldrin-plus', () => ({
@@ -38,6 +39,7 @@ const mockFiles = [
describe('Diff Stats Dropdown', () => {
let wrapper;
+ const focusInputMock = jest.fn();
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
wrapper = shallowMountExtended(DiffStatsDropdown, {
@@ -50,6 +52,9 @@ describe('Diff Stats Dropdown', () => {
stubs: {
GlSprintf,
GlDropdown,
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputMock },
+ }),
},
});
};
@@ -151,10 +156,8 @@ describe('Diff Stats Dropdown', () => {
});
it('should set the search input focus', () => {
- wrapper.vm.$refs.search.focusInput = jest.fn();
findChanged().vm.$emit('shown');
-
- expect(wrapper.vm.$refs.search.focusInput).toHaveBeenCalled();
+ expect(focusInputMock).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
index 6e2e854adae..36772ad03fe 100644
--- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -125,7 +125,8 @@ describe('EntitySelect', () => {
it('emits `input` event with the select value', async () => {
createComponent();
await selectGroup();
- expect(wrapper.emitted('input')[0]).toEqual(['1']);
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject(itemMock);
});
it(`uses the selected group's name as the toggle text`, async () => {
@@ -153,14 +154,14 @@ describe('EntitySelect', () => {
expect(findListbox().props('toggleText')).toBe(defaultToggleText);
});
- it('emits `input` event with `null` on reset', async () => {
+ it('emits `input` event with an empty object on reset', async () => {
createComponent();
await selectGroup();
findListbox().vm.$emit('reset');
await nextTick();
- expect(wrapper.emitted('input')[2]).toEqual([null]);
+ expect(Object.keys(wrapper.emitted('input')[2][0]).length).toBe(0);
});
});
});
diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
index 83560e367ea..ae551116560 100644
--- a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
@@ -39,6 +39,8 @@ describe('GroupSelect', () => {
const findEntitySelect = () => wrapper.findComponent(EntitySelect);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const handleInput = jest.fn();
+
// Helpers
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(GroupSelect, {
@@ -52,6 +54,9 @@ describe('GroupSelect', () => {
GlAlert,
EntitySelect,
},
+ listeners: {
+ input: handleInput,
+ },
});
};
const openListbox = () => findListbox().vm.$emit('shown');
@@ -132,4 +137,11 @@ describe('GroupSelect', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
});
+
+ it('forwards events to the parent scope via `v-on="$listeners"`', () => {
+ createComponent();
+ findEntitySelect().vm.$emit('input');
+
+ expect(handleInput).toHaveBeenCalledTimes(1);
+ });
});
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 0a174c98efb..9113152c975 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
@@ -45,6 +45,8 @@ describe('ProjectSelect', () => {
const findEntitySelect = () => wrapper.findComponent(EntitySelect);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const handleInput = jest.fn();
+
// Helpers
const createComponent = ({ props = {} } = {}) => {
wrapper = mountExtended(ProjectSelect, {
@@ -59,6 +61,9 @@ describe('ProjectSelect', () => {
GlAlert,
EntitySelect,
},
+ listeners: {
+ input: handleInput,
+ },
});
};
const openListbox = () => findListbox().vm.$emit('shown');
@@ -255,4 +260,11 @@ describe('ProjectSelect', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(FETCH_PROJECTS_ERROR);
});
+
+ it('forwards events to the parent scope via `v-on="$listeners"`', () => {
+ createComponent();
+ findEntitySelect().vm.$emit('input');
+
+ expect(handleInput).toHaveBeenCalledTimes(1);
+ });
});
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 c0cb17f0d16..00a412d9de8 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
@@ -125,46 +125,23 @@ describe('FilteredSearchBarRoot', () => {
});
describe('sortDirectionIcon', () => {
- it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.ascending,
- });
-
- expect(wrapper.vm.sortDirectionIcon).toBe('sort-lowest');
- });
-
- it('returns string "sort-highest" when `selectedSortDirection` is "descending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.descending,
+ it('renders `sort-highest` descending icon by default', () => {
+ expect(findGlButton().props('icon')).toBe('sort-highest');
+ expect(findGlButton().attributes()).toMatchObject({
+ 'aria-label': 'Sort direction: Descending',
+ title: 'Sort direction: Descending',
});
-
- expect(wrapper.vm.sortDirectionIcon).toBe('sort-highest');
});
- });
- describe('sortDirectionTooltip', () => {
- it('returns string "Sort direction: Ascending" when `selectedSortDirection` is "ascending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.ascending,
- });
-
- expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Ascending');
- });
+ it('renders `sort-lowest` ascending icon when the sort button is clicked', async () => {
+ findGlButton().vm.$emit('click');
+ await nextTick();
- it('returns string "Sort direction: Descending" when `selectedSortDirection` is "descending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.descending,
+ expect(findGlButton().props('icon')).toBe('sort-lowest');
+ expect(findGlButton().attributes()).toMatchObject({
+ 'aria-label': 'Sort direction: Ascending',
+ title: 'Sort direction: Ascending',
});
-
- expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending');
});
});
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 fb8cea09a9b..d34d7ff48c2 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
@@ -39,7 +39,6 @@ describe('CrmContactToken', () => {
Vue.use(VueApollo);
let wrapper;
- let fakeApollo;
const getBaseToken = () => wrapper.findComponent(BaseToken);
@@ -58,9 +57,8 @@ describe('CrmContactToken', () => {
listeners = {},
queryHandler = searchGroupCrmContactsQueryHandler,
} = {}) => {
- fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]);
-
wrapper = mount(CrmContactToken, {
+ apolloProvider: createMockApollo([[searchCrmContactsQuery, queryHandler]]),
propsData: {
config,
value,
@@ -75,14 +73,9 @@ describe('CrmContactToken', () => {
},
stubs,
listeners,
- apolloProvider: fakeApollo,
});
};
- afterEach(() => {
- fakeApollo = null;
- });
-
describe('methods', () => {
describe('fetchContacts', () => {
describe('for groups', () => {
@@ -160,9 +153,7 @@ describe('CrmContactToken', () => {
});
it('calls `createAlert` with alert error message when request fails', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@@ -173,12 +164,9 @@ describe('CrmContactToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
-
await waitForPromises();
expect(getBaseToken().props('suggestionsLoading')).toBe(false);
@@ -195,13 +183,7 @@ describe('CrmContactToken', () => {
value: { data: '1' },
});
- const baseTokenEl = wrapper.findComponent(BaseToken);
-
- expect(baseTokenEl.exists()).toBe(true);
- expect(baseTokenEl.props()).toMatchObject({
- suggestions: mockCrmContacts,
- getActiveTokenValue: wrapper.vm.getActiveContact,
- });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
});
it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
@@ -270,12 +252,9 @@ describe('CrmContactToken', () => {
it('emits listeners in the base-token', () => {
const mockInput = jest.fn();
- mountComponent({
- listeners: {
- input: mockInput,
- },
- });
- wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+ mountComponent({ listeners: { input: mockInput } });
+
+ getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
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 20369342220..17cf39e726c 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
@@ -39,7 +39,6 @@ describe('CrmOrganizationToken', () => {
Vue.use(VueApollo);
let wrapper;
- let fakeApollo;
const getBaseToken = () => wrapper.findComponent(BaseToken);
@@ -58,8 +57,8 @@ describe('CrmOrganizationToken', () => {
listeners = {},
queryHandler = searchGroupCrmOrganizationsQueryHandler,
} = {}) => {
- fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]);
wrapper = mount(CrmOrganizationToken, {
+ apolloProvider: createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]),
propsData: {
config,
value,
@@ -74,14 +73,9 @@ describe('CrmOrganizationToken', () => {
},
stubs,
listeners,
- apolloProvider: fakeApollo,
});
};
- afterEach(() => {
- fakeApollo = null;
- });
-
describe('methods', () => {
describe('fetchOrganizations', () => {
describe('for groups', () => {
@@ -159,9 +153,7 @@ describe('CrmOrganizationToken', () => {
});
it('calls `createAlert` when request fails', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@@ -172,9 +164,7 @@ describe('CrmOrganizationToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
@@ -194,13 +184,7 @@ describe('CrmOrganizationToken', () => {
value: { data: '1' },
});
- const baseTokenEl = wrapper.findComponent(BaseToken);
-
- expect(baseTokenEl.exists()).toBe(true);
- expect(baseTokenEl.props()).toMatchObject({
- suggestions: mockCrmOrganizations,
- getActiveTokenValue: wrapper.vm.getActiveOrganization,
- });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
});
it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
@@ -269,12 +253,9 @@ describe('CrmOrganizationToken', () => {
it('emits listeners in the base-token', () => {
const mockInput = jest.fn();
- mountComponent({
- listeners: {
- input: mockInput,
- },
- });
- wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+ mountComponent({ listeners: { input: mockInput } });
+
+ getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
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 397fd270344..b782a2b19da 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
@@ -7,7 +7,7 @@ describe('ListboxInput', () => {
// Props
const label = 'label';
- const decription = 'decription';
+ const description = 'description';
const name = 'name';
const defaultToggleText = 'defaultToggleText';
const items = [
@@ -34,7 +34,7 @@ describe('ListboxInput', () => {
wrapper = shallowMount(ListboxInput, {
propsData: {
label,
- decription,
+ description,
name,
defaultToggleText,
items,
@@ -72,8 +72,8 @@ describe('ListboxInput', () => {
expect(findGlFormGroup().attributes('label')).toBe(label);
});
- it('passes the decription to the form group', () => {
- expect(findGlFormGroup().attributes('decription')).toBe(decription);
+ it('passes the description to the form group', () => {
+ expect(findGlFormGroup().attributes('description')).toBe(description);
});
it('sets the input name', () => {
@@ -89,6 +89,26 @@ describe('ListboxInput', () => {
});
});
+ describe('props', () => {
+ it.each([true, false])("passes %s to the listbox's fluidWidth prop", (fluidWidth) => {
+ createComponent({ fluidWidth });
+
+ expect(findGlListbox().props('fluidWidth')).toBe(fluidWidth);
+ });
+
+ it.each(['right', 'left'])("passes %s to the listbox's placement prop", (placement) => {
+ createComponent({ placement });
+
+ expect(findGlListbox().props('placement')).toBe(placement);
+ });
+
+ it.each([true, false])("passes %s to the listbox's block prop", (block) => {
+ createComponent({ block });
+
+ expect(findGlListbox().props('block')).toBe(block);
+ });
+ });
+
describe('toggle text', () => {
it('uses the default toggle text while no value is selected', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
index aea25abb324..2bef6dd15df 100644
--- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -4,13 +4,9 @@ import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { updateText } from '~/lib/utils/text_markdown';
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
-jest.mock('~/lib/utils/text_markdown');
-
let wrapper;
let savedRepliesResp;
@@ -28,7 +24,6 @@ function createComponent(options = {}) {
const { mockApollo } = options;
return mountExtended(CommentTemplatesDropdown, {
- attachTo: '#root',
propsData: {
newCommentTemplatePath: '/new',
},
@@ -37,14 +32,6 @@ function createComponent(options = {}) {
}
describe('Comment templates dropdown', () => {
- beforeEach(() => {
- setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('fetches data when dropdown gets opened', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
@@ -56,7 +43,7 @@ describe('Comment templates dropdown', () => {
expect(savedRepliesResp).toHaveBeenCalled();
});
- it('adds content to textarea', async () => {
+ it('adds emits a select event on selecting a comment', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
@@ -66,11 +53,6 @@ describe('Comment templates dropdown', () => {
wrapper.find('.gl-new-dropdown-item').trigger('click');
- expect(updateText).toHaveBeenCalledWith({
- textArea: document.querySelector('textarea'),
- tag: savedRepliesResponse.data.currentUser.savedReplies.nodes[0].content,
- cursorOffset: 0,
- wrap: false,
- });
+ expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
index 693353ed604..712e78458c6 100644
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
@@ -1,25 +1,47 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlButton, GlLink, GlPopover } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { counter } from '~/vue_shared/components/markdown/utils';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { stubComponent } from 'helpers/stub_component';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+jest.mock('~/vue_shared/components/markdown/utils', () => ({
+ counter: jest.fn().mockReturnValue(0),
+}));
describe('vue_shared/component/markdown/editor_mode_switcher', () => {
let wrapper;
+ useLocalStorageSpy();
- const createComponent = ({ value } = {}) => {
- wrapper = shallowMount(EditorModeSwitcher, {
+ const createComponent = ({
+ value,
+ userCalloutDismisserSlotProps = { dismiss: jest.fn() },
+ } = {}) => {
+ wrapper = mount(EditorModeSwitcher, {
propsData: {
value,
},
+ stubs: {
+ UserCalloutDismisser: stubComponent(UserCalloutDismisser, {
+ render() {
+ return this.$scopedSlots.default(userCalloutDismisserSlotProps);
+ },
+ }),
+ },
});
};
const findSwitcherButton = () => wrapper.findComponent(GlButton);
+ const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
+ const findCalloutPopover = () => wrapper.findComponent(GlPopover);
describe.each`
- modeText | value | buttonText
- ${'Rich text'} | ${'richText'} | ${'Switch to Markdown'}
- ${'Markdown'} | ${'markdown'} | ${'Switch to rich text'}
- `('when $modeText', ({ modeText, value, buttonText }) => {
+ value | buttonText
+ ${'richText'} | ${'Switch to plain text editing'}
+ ${'markdown'} | ${'Switch to rich text editing'}
+ `('when $value', ({ value, buttonText }) => {
beforeEach(() => {
createComponent({ value });
});
@@ -28,10 +50,66 @@ describe('vue_shared/component/markdown/editor_mode_switcher', () => {
expect(findSwitcherButton().text()).toEqual(buttonText);
});
- it('emits event on click', () => {
- findSwitcherButton(modeText).vm.$emit('click');
+ it('emits event on click', async () => {
+ await nextTick();
+ findSwitcherButton().vm.$emit('click');
+
+ expect(wrapper.emitted().switch).toEqual([[false]]);
+ });
+ });
+
+ describe('rich text editor callout', () => {
+ let dismiss;
+
+ beforeEach(() => {
+ dismiss = jest.fn();
+ createComponent({ value: 'markdown', userCalloutDismisserSlotProps: { dismiss } });
+ });
+
+ it('does not skip the user_callout_dismisser query', () => {
+ expect(findUserCalloutDismisser().props()).toMatchObject({
+ skipQuery: false,
+ featureName: 'rich_text_editor',
+ });
+ });
+
+ it('mounts new rich text editor popover', () => {
+ expect(findCalloutPopover().props()).toMatchObject({
+ showCloseButton: '',
+ triggers: 'manual',
+ target: 'switch-to-rich-text-editor',
+ });
+ });
+
+ it('dismisses the callout and emits "switch" event when popover close button is clicked', async () => {
+ await findCalloutPopover().findComponent(GlLink).vm.$emit('click');
+
+ expect(wrapper.emitted().switch).toEqual([[true]]);
+ expect(dismiss).toHaveBeenCalled();
+ });
+
+ it('dismisses the callout when action button is clicked', () => {
+ findSwitcherButton().vm.$emit('click');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
+ it('does not show the callout if rich text is already enabled', async () => {
+ await wrapper.setProps({ value: 'richText' });
+
+ expect(findCalloutPopover().props()).toMatchObject({
+ show: false,
+ });
+ });
+
+ it('does not show the callout if already displayed once on the page', () => {
+ counter.mockReturnValue(1);
+
+ createComponent({ value: 'markdown' });
- expect(wrapper.emitted().input).toEqual([[]]);
+ expect(findCalloutPopover().props()).toMatchObject({
+ show: false,
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index b29f0d58d77..4ade8f28fd0 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -65,6 +65,16 @@ describe('Markdown field component', () => {
enablePreview,
restrictedToolBarItems,
showContentEditorSwitcher,
+ supportsQuickActions: true,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
},
},
);
@@ -206,12 +216,12 @@ describe('Markdown field component', () => {
expect(findMarkdownToolbar().props()).toEqual({
canAttachFile: true,
markdownDocsPath,
- quickActionsDocsPath: '',
showCommentToolBar: true,
+ showContentEditorSwitcher: false,
});
expect(findMarkdownHeader().props()).toMatchObject({
- showContentEditorSwitcher: false,
+ supportsQuickActions: true,
});
});
});
@@ -368,13 +378,13 @@ describe('Markdown field component', () => {
it('defaults to false', () => {
createSubject();
- expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(false);
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false);
});
it('passes showContentEditorSwitcher', () => {
createSubject({ showContentEditorSwitcher: true });
- expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(true);
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 48fe5452e74..eb728879fb7 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -1,22 +1,28 @@
import $ from 'jquery';
import { nextTick } from 'vue';
-import { GlToggle } from '@gitlab/ui';
+import { GlToggle, GlButton } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
+import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.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';
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { updateText } from '~/lib/utils/text_markdown';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/text_markdown');
describe('Markdown field header component', () => {
let wrapper;
- const createWrapper = (props) => {
+ const createWrapper = ({ props = {}, provide = {}, attachTo = document.body } = {}) => {
wrapper = shallowMountExtended(HeaderComponent, {
+ attachTo,
propsData: {
previewMarkdown: false,
...props,
},
stubs: { GlToggle },
+ provide,
});
};
@@ -28,6 +34,7 @@ describe('Markdown field header component', () => {
.filter((button) => button.props(prop) === value)
.at(0);
const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton);
+ const findCommentTemplatesDropdown = () => wrapper.findComponent(CommentTemplatesDropdown);
beforeEach(() => {
window.gl = {
@@ -65,6 +72,39 @@ describe('Markdown field header component', () => {
});
});
+ it('renders correct title on non MacOS systems', () => {
+ window.gl = {
+ client: {
+ isMac: false,
+ },
+ };
+
+ createWrapper();
+
+ const buttons = [
+ 'Insert suggestion',
+ 'Add bold text (Ctrl+B)',
+ 'Add italic text (Ctrl+I)',
+ 'Add strikethrough text (Ctrl+Shift+X)',
+ 'Insert a quote',
+ 'Insert code',
+ 'Add a link (Ctrl+K)',
+ 'Add a bullet list',
+ 'Add a numbered list',
+ 'Add a checklist',
+ 'Indent line (Ctrl+])',
+ 'Outdent line (Ctrl+[)',
+ 'Add a collapsible section',
+ 'Add a table',
+ 'Go full screen',
+ ];
+ const elements = findToolbarButtons();
+
+ elements.wrappers.forEach((buttonEl, index) => {
+ expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
+ });
+ });
+
it('renders "Attach a file or image" button using gl-button', () => {
const button = wrapper.findByTestId('button-attach-file');
@@ -92,15 +132,16 @@ describe('Markdown field header component', () => {
});
it('shows markdown preview when previewMarkdown is true', () => {
- createWrapper({ previewMarkdown: true });
+ createWrapper({ props: { previewMarkdown: true } });
expect(findPreviewToggle().text()).toBe('Continue editing');
});
it('hides toolbar in preview mode', () => {
- createWrapper({ previewMarkdown: true });
+ createWrapper({ props: { previewMarkdown: true } });
- expect(findToolbar().classes().includes('gl-display-none!')).toBe(true);
+ // only one button is rendered in preview mode
+ expect(findToolbar().findAllComponents(GlButton)).toHaveLength(1);
});
it('emits toggle markdown event when clicking preview toggle', async () => {
@@ -150,7 +191,9 @@ describe('Markdown field header component', () => {
it('does not render suggestion button if `canSuggest` is set to false', () => {
createWrapper({
- canSuggest: false,
+ props: {
+ canSuggest: false,
+ },
});
expect(wrapper.find('.js-suggestion-btn').exists()).toBe(false);
@@ -158,7 +201,9 @@ describe('Markdown field header component', () => {
it('hides markdown preview when previewMarkdown property is false', () => {
createWrapper({
- enablePreview: false,
+ props: {
+ enablePreview: false,
+ },
});
expect(wrapper.findByTestId('preview-toggle').exists()).toBe(false);
@@ -173,7 +218,9 @@ describe('Markdown field header component', () => {
it('restricts items as per input', () => {
createWrapper({
- restrictedToolBarItems: ['quote'],
+ props: {
+ restrictedToolBarItems: ['quote'],
+ },
});
expect(findToolbarButtons().length).toBe(defaultCount - 1);
@@ -192,9 +239,11 @@ describe('Markdown field header component', () => {
beforeEach(() => {
createWrapper({
- drawioEnabled: true,
- uploadsPath,
- markdownPreviewPath,
+ props: {
+ drawioEnabled: true,
+ uploadsPath,
+ markdownPreviewPath,
+ },
});
});
@@ -206,17 +255,46 @@ describe('Markdown field header component', () => {
});
});
- describe('with content editor switcher', () => {
+ describe('when selecting a saved reply from the comment templates dropdown', () => {
beforeEach(() => {
+ setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('updates the textarea with the saved comment', async () => {
createWrapper({
- showContentEditorSwitcher: true,
+ attachTo: '#root',
+ provide: {
+ newCommentTemplatePath: 'some/path',
+ glFeatures: {
+ savedReplies: true,
+ },
+ },
+ });
+
+ await findCommentTemplatesDropdown().vm.$emit('select', 'Some saved comment');
+
+ expect(updateText).toHaveBeenCalledWith({
+ textArea: document.querySelector('textarea'),
+ tag: 'Some saved comment',
+ cursorOffset: 0,
+ wrap: false,
});
});
- it('re-emits event from switcher', () => {
- wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText');
+ it('does not show the saved replies button if newCommentTemplatePath is not defined', () => {
+ createWrapper({
+ provide: {
+ glFeatures: {
+ savedReplies: true,
+ },
+ },
+ });
- expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ expect(findCommentTemplatesDropdown().exists()).toBe(false);
});
});
});
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 e54e261b8e4..31c0fa6f699 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -21,6 +21,7 @@ import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji');
jest.mock('autosize');
+jest.mock('~/lib/graphql');
describe('vue_shared/component/markdown/markdown_editor', () => {
useLocalStorageSpy();
@@ -29,7 +30,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const value = 'test markdown';
const renderMarkdownPath = '/api/markdown';
const markdownDocsPath = '/help/markdown';
- const quickActionsDocsPath = '/help/quickactions';
const enableAutocomplete = true;
const enablePreview = false;
const formFieldId = 'markdown_field';
@@ -43,7 +43,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
value,
renderMarkdownPath,
markdownDocsPath,
- quickActionsDocsPath,
enableAutocomplete,
autocompleteDataSources,
enablePreview,
@@ -65,6 +64,15 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
BubbleMenu: stubComponent(BubbleMenu),
...stubs,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
@@ -110,7 +118,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findMarkdownField().props()).toMatchObject({
autocompleteDataSources,
markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
+ supportsQuickActions: true,
canAttachFile: true,
enableAutocomplete,
textareaValue: value,
@@ -120,7 +128,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
- // quarantine flaky spec:https://gitlab.com/gitlab-org/gitlab/-/issues/412618
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/412618
// eslint-disable-next-line jest/no-disabled-tests
it.skip('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
@@ -131,7 +139,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(mock.history.post[0].url).toContain(`render_quick_actions=true`);
});
- // quarantine flaky spec: https://gitlab.com/gitlab-org/gitlab/-/issues/411565
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/411565
// eslint-disable-next-line jest/no-disabled-tests
it.skip('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => {
buildWrapper({ propsData: { supportsQuickActions: false } });
@@ -145,27 +153,31 @@ 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('Switch to rich text');
+ expect(findMarkdownField().text()).toContain('Switch to rich text editing');
});
it('hides content editor switcher when contentEditorEnabled prop is false', () => {
buildWrapper({ propsData: { enableContentEditor: false } });
- expect(findMarkdownField().text()).not.toContain('Switch to rich text');
+ expect(findMarkdownField().text()).not.toContain('Switch to rich text editing');
});
it('passes down any additional props to markdown field component', () => {
- const propsData = {
+ const codeSuggestionsConfig = {
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' },
+ propsData: {
+ codeSuggestionsConfig,
+ myCustomProp: 'myCustomValue',
+ 'data-testid': 'custom id',
+ },
});
- expect(findMarkdownField().props()).toMatchObject(propsData);
+ expect(findMarkdownField().props()).toMatchObject(codeSuggestionsConfig);
expect(findMarkdownField().vm.$attrs).toMatchObject({
myCustomProp: 'myCustomValue',
@@ -201,7 +213,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined);
});
- // quarantine flaky spec: https://gitlab.com/gitlab-org/gitlab/-/issues/404734
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/404734
// eslint-disable-next-line jest/no-disabled-tests
it.skip('disables content editor when disabled prop is true', async () => {
buildWrapper({ propsData: { disabled: true } });
@@ -436,8 +448,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
describe('when contentEditor is disabled', () => {
it('resets the editingMode to markdownField', () => {
- localStorage.setItem('gl-markdown-editor-mode', 'contentEditor');
-
buildWrapper({ propsData: { autosaveKey: 'issue/1234', enableContentEditor: false } });
expect(wrapper.vm.editingMode).toBe(EDITING_MODE_MARKDOWN_FIELD);
diff --git a/spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js b/spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js
new file mode 100644
index 00000000000..cd73ef6892a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js
@@ -0,0 +1,157 @@
+import { nextTick } from 'vue';
+import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+describe('NonGitlabMarkdown', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(Markdown, {
+ propsData,
+ });
+ };
+
+ const codeBlockContent = 'stages:\n - build\n - test\n - deploy\n';
+ const codeBlockLanguage = 'yaml';
+ const nonCodeContent =
+ "Certainly! Here's an updated GitLab CI/CD configuration in YAML format that includes Kubernetes deployment:";
+ const testMarkdownWithCodeBlock = `${nonCodeContent}\n\n\`\`\`${codeBlockLanguage}\n${codeBlockContent}\n\`\`\`\n\nIn this updated configuration, we have added a \`deploy\` job that deploys the Python app to a Kubernetes cluster. The \`script\` section of the job includes commands to authenticate with GCP, set the project and zone, configure kubectl to use the GKE cluster, and deploy the application using a deployment.yaml file.\n\nNote that you will need to modify this configuration to fit your specific deployment needs, including replacing the placeholders (\`<PROJECT_ID>\`, \`<COMPUTE_ZONE>\`, \`<CLUSTER_NAME>\`, and \`<COMPUTE_REGION>\`) with your GCP and Kubernetes deployment information, and creating the deployment.yaml file with your Kubernetes deployment configuration.`;
+ const codeOnlyMarkdown = `\`\`\`${codeBlockLanguage}\n${codeBlockContent}\n\`\`\``;
+ const markdownWithMultipleCodeSnippets = `${testMarkdownWithCodeBlock}\n${testMarkdownWithCodeBlock}`;
+ const codeBlockNoLanguage = `
+ \`\`\`
+ const foo = 'bar';
+ \`\`\`
+ `;
+
+ const findCodeBlock = () => wrapper.findComponent(CodeBlockHighlighted);
+ const findCopyCodeButton = () => wrapper.findComponent(ModalCopyButton);
+ const findCodeBlockWrapper = () => wrapper.findByTestId('code-block-wrapper');
+ const findMarkdownBlock = () => wrapper.findByTestId('non-code-markdown');
+
+ describe('rendering markdown without code snippet', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: nonCodeContent } });
+ });
+ it('should render non-code content', () => {
+ const markdownBlock = findMarkdownBlock();
+ expect(markdownBlock.exists()).toBe(true);
+ expect(markdownBlock.text()).toBe(nonCodeContent);
+ });
+ it('should not render code block', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(false);
+ });
+ });
+
+ describe('rendering code snippet without other markdown', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: codeOnlyMarkdown } });
+ });
+ it('should not render non-code content', () => {
+ const markdownBlock = findMarkdownBlock();
+ expect(markdownBlock.exists()).toBe(false);
+ });
+ it('should render code block', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(true);
+ });
+ });
+
+ describe('rendering code snippet with no language specified', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: codeBlockNoLanguage } });
+ });
+
+ it('should render code block', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(true);
+ expect(codeBlock.props('language')).toBe('text');
+ });
+ });
+
+ describe.each`
+ markdown | codeBlocksCount | markdownBlocksCount
+ ${testMarkdownWithCodeBlock} | ${1} | ${2}
+ ${markdownWithMultipleCodeSnippets} | ${2} | ${3}
+ ${codeOnlyMarkdown} | ${1} | ${0}
+ ${nonCodeContent} | ${0} | ${1}
+ `(
+ 'extracting tokens in markdownBlocks computed',
+ ({ markdown, codeBlocksCount, markdownBlocksCount }) => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown } });
+ });
+
+ it('should create correct number of tokens', () => {
+ const findAllCodeBlocks = () => wrapper.findAllByTestId('code-block-wrapper');
+ const findAllMarkdownBlocks = () => wrapper.findAllByTestId('non-code-markdown');
+
+ expect(findAllCodeBlocks()).toHaveLength(codeBlocksCount);
+ expect(findAllMarkdownBlocks()).toHaveLength(markdownBlocksCount);
+ });
+ },
+ );
+
+ describe('rendering markdown with multiple code snippets', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: markdownWithMultipleCodeSnippets } });
+ });
+
+ it('should render code block with correct props', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(true);
+ expect(codeBlock.props()).toEqual(
+ expect.objectContaining({
+ language: codeBlockLanguage,
+ code: codeBlockContent,
+ }),
+ );
+ expect(wrapper.findAllComponents(CodeBlockHighlighted)).toHaveLength(2);
+ });
+
+ it('should not show copy code button', () => {
+ const copyCodeButton = findCopyCodeButton();
+ expect(copyCodeButton.exists()).toBe(false);
+ });
+
+ it('should render non-code content', () => {
+ const markdownBlock = findMarkdownBlock();
+ expect(markdownBlock.exists()).toBe(true);
+ expect(markdownBlock.text()).toContain(nonCodeContent);
+ });
+
+ describe('copy code button', () => {
+ beforeEach(() => {
+ const codeBlock = findCodeBlockWrapper();
+ codeBlock.trigger('mouseenter');
+ });
+
+ it('should render only one copy button per code block', () => {
+ const copyCodeButtons = wrapper.findAllComponents(ModalCopyButton);
+ expect(copyCodeButtons).toHaveLength(1);
+ });
+
+ it('should render code block button with correct props', () => {
+ const copyCodeButton = findCopyCodeButton();
+ expect(copyCodeButton.exists()).toBe(true);
+ expect(copyCodeButton.props()).toEqual(
+ expect.objectContaining({
+ text: codeBlockContent,
+ title: 'Copy code',
+ }),
+ );
+ });
+
+ it('should hide code block button on mouseleave', async () => {
+ const codeBlock = findCodeBlockWrapper();
+ codeBlock.trigger('mouseleave');
+ await nextTick();
+ const copyCodeButton = findCopyCodeButton();
+ expect(copyCodeButton.exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 2489421b697..5bf11ff2b26 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,18 +1,33 @@
import { mount } from '@vue/test-utils';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { updateText } from '~/lib/utils/text_markdown';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/text_markdown');
describe('toolbar', () => {
let wrapper;
- const createMountedWrapper = (props = {}) => {
+ const createWrapper = (props = {}, attachTo = document.body) => {
wrapper = mount(Toolbar, {
+ attachTo,
propsData: { markdownDocsPath: '', ...props },
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
describe('user can attach file', () => {
beforeEach(() => {
- createMountedWrapper();
+ createWrapper();
});
it('should render uploading-container', () => {
@@ -22,7 +37,7 @@ describe('toolbar', () => {
describe('user cannot attach file', () => {
beforeEach(() => {
- createMountedWrapper({ canAttachFile: false });
+ createWrapper({ canAttachFile: false });
});
it('should not render uploading-container', () => {
@@ -32,15 +47,63 @@ describe('toolbar', () => {
describe('comment tool bar settings', () => {
it('does not show comment tool bar div', () => {
- createMountedWrapper({ showCommentToolBar: false });
+ createWrapper({ showCommentToolBar: false });
expect(wrapper.find('.comment-toolbar').exists()).toBe(false);
});
it('shows comment tool bar by default', () => {
- createMountedWrapper();
+ createWrapper();
expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
});
});
+
+ describe('with content editor switcher', () => {
+ beforeEach(() => {
+ setHTMLFixture(
+ '<div class="md-area"><textarea>some value</textarea><div id="root"></div></div>',
+ );
+ createWrapper(
+ {
+ showContentEditorSwitcher: true,
+ },
+ '#root',
+ );
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('re-emits event from switcher', () => {
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch');
+
+ expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ expect(updateText).not.toHaveBeenCalled();
+ });
+
+ it('does not insert a template text if textarea has some value', () => {
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch', true);
+
+ expect(updateText).not.toHaveBeenCalled();
+ });
+
+ it('inserts a "getting started with rich text" template when switched for the first time', () => {
+ document.querySelector('textarea').value = '';
+
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch', true);
+
+ expect(updateText).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tag: `### Rich text editor
+
+Try out **styling** _your_ content right here or read the [direction](https://about.gitlab.com/direction/plan/knowledge/content_editor/).`,
+ textArea: document.querySelector('textarea'),
+ cursorOffset: 0,
+ wrap: false,
+ }),
+ );
+ });
+ });
});
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 a116233a065..f04e1976a5f 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
@@ -38,7 +38,6 @@ describe('NewResourceDropdown component', () => {
};
const mountComponent = ({
- search = '',
query = searchUserProjectsWithIssuesEnabledQuery,
queryResponse = searchProjectsQueryResponse,
mountFn = shallowMount,
@@ -47,16 +46,14 @@ describe('NewResourceDropdown component', () => {
const requestHandlers = [[query, jest.fn().mockResolvedValue(queryResponse)]];
const apolloProvider = createMockApollo(requestHandlers);
- return mountFn(NewResourceDropdown, {
+ wrapper = mountFn(NewResourceDropdown, {
apolloProvider,
propsData,
- data() {
- return { search };
- },
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const showDropdown = async () => {
findDropdown().vm.$emit('shown');
@@ -70,13 +67,13 @@ describe('NewResourceDropdown component', () => {
});
it('renders a split dropdown', () => {
- wrapper = mountComponent();
+ mountComponent();
expect(findDropdown().props('split')).toBe(true);
});
it('renders a label for the dropdown toggle button', () => {
- wrapper = mountComponent();
+ mountComponent();
expect(findDropdown().attributes('toggle-text')).toBe(
NewResourceDropdown.i18n.toggleButtonLabel,
@@ -84,7 +81,7 @@ describe('NewResourceDropdown component', () => {
});
it('focuses on input when dropdown is shown', async () => {
- wrapper = mountComponent({ mountFn: mount });
+ mountComponent({ mountFn: mount });
const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
@@ -99,7 +96,7 @@ describe('NewResourceDropdown component', () => {
${'within a group'} | ${withinGroupProps} | ${searchProjectsWithinGroupQuery} | ${searchProjectsWithinGroupQueryResponse} | ${emptySearchProjectsWithinGroupQueryResponse}
`('$description', ({ propsData, query, queryResponse, emptyResponse }) => {
it('renders projects options', async () => {
- wrapper = mountComponent({ mountFn: mount, query, queryResponse, propsData });
+ mountComponent({ mountFn: mount, query, queryResponse, propsData });
await showDropdown();
const listItems = wrapper.findAll('li');
@@ -110,14 +107,14 @@ describe('NewResourceDropdown component', () => {
});
it('renders `No matches found` when there are no matches', async () => {
- wrapper = mountComponent({
- search: 'no matches',
+ mountComponent({
query,
queryResponse: emptyResponse,
mountFn: mount,
propsData,
});
+ await findInput().vm.$emit('input', 'no matches');
await showDropdown();
expect(wrapper.find('li').text()).toBe(NewResourceDropdown.i18n.noMatchesFound);
@@ -133,7 +130,7 @@ describe('NewResourceDropdown component', () => {
({ resourceType, expectedDefaultLabel, expectedPath, expectedLabel }) => {
describe('when no project is selected', () => {
beforeEach(() => {
- wrapper = mountComponent({
+ mountComponent({
query,
queryResponse,
propsData: { ...propsData, resourceType },
@@ -151,7 +148,7 @@ describe('NewResourceDropdown component', () => {
describe('when a project is selected', () => {
beforeEach(async () => {
- wrapper = mountComponent({
+ mountComponent({
mountFn: mount,
query,
queryResponse,
@@ -159,7 +156,7 @@ describe('NewResourceDropdown component', () => {
});
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
});
it('dropdown button is a link', () => {
@@ -178,12 +175,12 @@ describe('NewResourceDropdown component', () => {
describe('without localStorage', () => {
beforeEach(() => {
- wrapper = mountComponent({ mountFn: mount });
+ mountComponent({ mountFn: mount });
});
it('does not attempt to save the selected project to the localStorage', async () => {
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
expect(localStorage.setItem).not.toHaveBeenCalled();
});
@@ -198,7 +195,7 @@ describe('NewResourceDropdown component', () => {
name: project1.name,
}),
);
- wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
await nextTick();
const dropdown = findDropdown();
@@ -216,7 +213,7 @@ describe('NewResourceDropdown component', () => {
name: project1.name,
}),
);
- wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
await nextTick();
const dropdown = findDropdown();
@@ -228,12 +225,12 @@ describe('NewResourceDropdown component', () => {
describe.each(RESOURCE_TYPES)('with resource type %s', (resourceType) => {
it('computes the local storage key without a group', async () => {
- wrapper = mountComponent({
+ mountComponent({
mountFn: mount,
propsData: { resourceType, withLocalStorage: true },
});
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
await nextTick();
expect(localStorage.setItem).toHaveBeenLastCalledWith(
@@ -244,12 +241,12 @@ describe('NewResourceDropdown component', () => {
it('computes the local storage key with a group', async () => {
const groupId = '22';
- wrapper = mountComponent({
+ mountComponent({
mountFn: mount,
propsData: { groupId, resourceType, withLocalStorage: true },
});
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
await nextTick();
expect(localStorage.setItem).toHaveBeenLastCalledWith(
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 a27877e7ba8..e5b641c61fd 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
@@ -300,6 +300,7 @@ describe('AlertManagementEmptyState', () => {
unique: true,
symbol: '@',
token: UserToken,
+ dataType: 'user',
operators: OPERATORS_IS,
fetchPath: '/link',
fetchUsers: expect.any(Function),
@@ -311,6 +312,7 @@ describe('AlertManagementEmptyState', () => {
unique: true,
symbol: '@',
token: UserToken,
+ dataType: 'user',
operators: OPERATORS_IS,
fetchPath: '/link',
fetchUsers: expect.any(Function),
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 3e4d5c558f6..0e387d1c139 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -38,6 +38,7 @@ describe('ProjectsListItem', () => {
const findProjectTopics = () => wrapper.findByTestId('project-topics');
const findPopover = () => findProjectTopics().findComponent(GlPopover);
const findProjectDescription = () => wrapper.findByTestId('project-description');
+ const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
it('renders project avatar', () => {
createComponent();
@@ -48,11 +49,11 @@ describe('ProjectsListItem', () => {
label: project.name,
labelLink: project.webUrl,
});
+
expect(avatarLabeled.attributes()).toMatchObject({
'entity-id': project.id.toString(),
'entity-name': project.name,
shape: 'rect',
- size: '48',
});
});
@@ -66,6 +67,19 @@ describe('ProjectsListItem', () => {
expect(tooltip.value).toBe(PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]);
});
+ describe('when visibility is not provided', () => {
+ it('does not render visibility icon', () => {
+ const { visibility, ...projectWithoutVisibility } = project;
+ createComponent({
+ propsData: {
+ project: projectWithoutVisibility,
+ },
+ });
+
+ expect(findVisibilityIcon().exists()).toBe(false);
+ });
+ });
+
it('renders access role badge', () => {
createComponent();
@@ -113,6 +127,19 @@ describe('ProjectsListItem', () => {
expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(project.updatedAt);
});
+ describe('when updated at is not available', () => {
+ it('does not render updated at', () => {
+ const { updatedAt, ...projectWithoutUpdatedAt } = project;
+ createComponent({
+ propsData: {
+ project: projectWithoutUpdatedAt,
+ },
+ });
+
+ expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(false);
+ });
+ });
+
describe('when issues are enabled', () => {
it('renders issues count', () => {
createComponent();
@@ -263,4 +290,20 @@ describe('ProjectsListItem', () => {
expect(findProjectDescription().exists()).toBe(false);
});
});
+
+ describe('when `showProjectIcon` prop is `true`', () => {
+ it('shows project icon', () => {
+ createComponent({ propsData: { showProjectIcon: true } });
+
+ expect(wrapper.findByTestId('project-icon').exists()).toBe(true);
+ });
+ });
+
+ describe('when `showProjectIcon` prop is `false`', () => {
+ it('does not show project icon', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('project-icon').exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
index 9380e19c39e..a0adbb89894 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
@@ -28,6 +28,7 @@ describe('ProjectsList', () => {
expect(expectedProps).toEqual(
defaultPropsData.projects.map((project) => ({
project,
+ showProjectIcon: false,
})),
);
});
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 298fa163d59..4a230f72f21 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/vue_shared/components/registry/list_item.vue';
describe('list item', () => {
@@ -27,6 +28,9 @@ describe('list item', () => {
'right-action': '<div data-testid="right-action" />',
...slots,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -90,6 +94,48 @@ describe('list item', () => {
expect(findToggleDetailsButton().exists()).toBe(true);
});
+ describe('when visible', () => {
+ beforeEach(async () => {
+ mountComponent({}, { 'details-foo': '<span></span>' });
+ await nextTick();
+ });
+
+ it('has tooltip', () => {
+ const tooltip = getBinding(findToggleDetailsButton().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(findToggleDetailsButton().attributes('title')).toBe(
+ component.i18n.toggleDetailsLabel,
+ );
+ });
+
+ it('has correct attributes and props', () => {
+ expect(findToggleDetailsButton().props()).toMatchObject({
+ selected: false,
+ });
+
+ expect(findToggleDetailsButton().attributes()).toMatchObject({
+ title: component.i18n.toggleDetailsLabel,
+ 'aria-label': component.i18n.toggleDetailsLabel,
+ });
+ });
+
+ it('has correct attributes and props when clicked', async () => {
+ findToggleDetailsButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleDetailsButton().props()).toMatchObject({
+ selected: true,
+ });
+
+ expect(findToggleDetailsButton().attributes()).toMatchObject({
+ title: component.i18n.toggleDetailsLabel,
+ 'aria-label': component.i18n.toggleDetailsLabel,
+ 'aria-expanded': 'true',
+ });
+ });
+ });
+
it('is hidden without details slot', () => {
mountComponent();
expect(findToggleDetailsButton().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
index 94823bb640b..b94d8c1de21 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import RunnerDockerInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue';
+import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
describe('RunnerDockerInstructions', () => {
let wrapper;
@@ -25,8 +26,6 @@ describe('RunnerDockerInstructions', () => {
});
it('renders link', () => {
- expect(findButton().attributes('href')).toBe(
- 'https://docs.gitlab.com/runner/install/docker.html',
- );
+ expect(findButton().attributes('href')).toBe(`${DOCS_URL}/runner/install/docker.html`);
});
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
index 9d6658e002c..f0b033a2ca2 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import RunnerKubernetesInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue';
+import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
describe('RunnerKubernetesInstructions', () => {
let wrapper;
@@ -25,8 +26,6 @@ describe('RunnerKubernetesInstructions', () => {
});
it('renders link', () => {
- expect(findButton().attributes('href')).toBe(
- 'https://docs.gitlab.com/runner/install/kubernetes.html',
- );
+ expect(findButton().attributes('href')).toBe(`${DOCS_URL}/runner/install/kubernetes.html`);
});
});
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 2eaf46e6209..e307e53147b 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
@@ -90,14 +90,6 @@ describe('RunnerInstructionsModal component', () => {
await waitForPromises();
});
- it('should not show alert', () => {
- expect(findAlert().exists()).toBe(false);
- });
-
- it('should not show deprecation alert', () => {
- expect(findAlert('warning').exists()).toBe(false);
- });
-
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
@@ -112,19 +104,8 @@ describe('RunnerInstructionsModal component', () => {
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
});
- describe.each`
- glFeatures | deprecationAlertExists
- ${{}} | ${false}
- ${{ createRunnerWorkflowForAdmin: true }} | ${true}
- ${{ createRunnerWorkflowForNamespace: true }} | ${true}
- `('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
- beforeEach(() => {
- createComponent({ provide: { glFeatures } });
- });
-
- it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
- expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
- });
+ it('alert is shown', () => {
+ expect(findAlert('warning').exists()).toBe(true);
});
describe('when the modal resizes', () => {
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
deleted file mode 100644
index 66d27b5d605..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
+++ /dev/null
@@ -1,144 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
-<span>
- Security scanning detected
- <strong>
- 1
- </strong>
- potential vulnerability
- <span
- class="gl-font-sm"
- >
- <span>
- <span
- class="gl-pl-4"
- >
-
- 0 Critical
-
- </span>
- </span>
-
- <span>
- <strong
- class="gl-text-red-600 gl-px-2"
- >
-
- 1 High
-
- </strong>
- </span>
- and
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 Others
-
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
-<span>
- Security scanning detected
- <strong>
- 1
- </strong>
- potential vulnerability
- <span
- class="gl-font-sm"
- >
- <span>
- <strong
- class="gl-text-red-800 gl-pl-4"
- >
-
- 1 Critical
-
- </strong>
- </span>
-
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 High
-
- </span>
- </span>
- and
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 Others
-
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = `
-<span>
- Security scanning detected
- <strong>
- 3
- </strong>
- potential vulnerabilities
- <span
- class="gl-font-sm"
- >
- <span>
- <strong
- class="gl-text-red-800 gl-pl-4"
- >
-
- 1 Critical
-
- </strong>
- </span>
-
- <span>
- <strong
- class="gl-text-red-600 gl-px-2"
- >
-
- 2 High
-
- </strong>
- </span>
- and
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 Others
-
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"message": ""} interpolates correctly 1`] = `
-<span>
-
- <!---->
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"message": "foo"} interpolates correctly 1`] = `
-<span>
- foo
- <!---->
-</span>
-`;
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
deleted file mode 100644
index 6eebd129beb..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import {
- expectedDownloadDropdownPropsWithTitle,
- securityReportMergeRequestDownloadPathsQueryResponse,
-} from 'jest/vue_shared/security_reports/mock_data';
-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 {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SECRET_DETECTION,
-} 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('~/alert');
-
-describe('Merge request artifact Download', () => {
- let wrapper;
-
- const defaultProps = {
- reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
- targetProjectFullPath: '/path',
- mrIid: 123,
- };
-
- const createWrapper = ({ propsData, options }) => {
- wrapper = shallowMount(Component, {
- stubs: {
- SecurityReportDownloadDropdown,
- },
- propsData: {
- ...defaultProps,
- ...propsData,
- },
- ...options,
- });
- };
-
- const pendingHandler = () => new Promise(() => {});
- const successHandler = () =>
- Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
- const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
- const createMockApolloProvider = (handler) => {
- Vue.use(VueApollo);
- const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
-
- return createMockApollo(requestHandlers);
- };
-
- const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
-
- describe('given the query is loading', () => {
- beforeEach(() => {
- createWrapper({
- options: {
- apolloProvider: createMockApolloProvider(pendingHandler),
- },
- });
- });
-
- it('loading is true', () => {
- expect(findDownloadDropdown().props('loading')).toBe(true);
- });
- });
-
- describe('given the query loads successfully', () => {
- beforeEach(() => {
- createWrapper({
- options: {
- apolloProvider: createMockApolloProvider(successHandler),
- },
- });
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithTitle);
- });
- });
-
- describe('given the query fails', () => {
- beforeEach(() => {
- createWrapper({
- options: {
- apolloProvider: createMockApolloProvider(failureHandler),
- },
- });
- });
-
- it('calls createAlert correctly', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: Component.i18n.apiError,
- captureError: true,
- error: expect.any(Error),
- });
- });
-
- it('renders nothing', () => {
- expect(findDownloadDropdown().props('artifacts')).toEqual([]);
- });
- });
-});
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
deleted file mode 100644
index 2f6e633fb34..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { GlLink, GlPopover } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
-
-const helpPath = '/docs';
-const discoverProjectSecurityPath = '/discoverProjectSecurityPath';
-
-describe('HelpIcon component', () => {
- let wrapper;
-
- const createWrapper = (props) => {
- wrapper = shallowMount(HelpIcon, {
- propsData: {
- helpPath,
- ...props,
- },
- });
- };
-
- const findLink = () => wrapper.findComponent(GlLink);
- const findPopover = () => wrapper.findComponent(GlPopover);
- const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' });
-
- describe('given a help path only', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('does not render a popover', () => {
- expect(findPopover().exists()).toBe(false);
- });
-
- it('renders a help link', () => {
- expect(findLink().attributes()).toMatchObject({
- href: helpPath,
- target: '_blank',
- });
- });
- });
-
- describe('given a help path and discover project security path', () => {
- beforeEach(() => {
- createWrapper({ discoverProjectSecurityPath });
- });
-
- it('renders a popover', () => {
- const popover = findPopover();
- expect(popover.props('target')()).toBe(findPopoverTarget().element);
- expect(popover.attributes()).toMatchObject({
- title: HelpIcon.i18n.upgradeToManageVulnerabilities,
- triggers: 'click blur',
- });
- expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract);
- });
-
- it('renders a link to the discover path', () => {
- expect(findLink().attributes()).toMatchObject({
- href: discoverProjectSecurityPath,
- target: '_blank',
- });
- });
- });
-});
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
deleted file mode 100644
index 61cdc329220..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
-import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
-
-describe('SecuritySummary component', () => {
- let wrapper;
-
- const createWrapper = (message) => {
- wrapper = shallowMount(SecuritySummary, {
- propsData: { message },
- stubs: {
- GlSprintf,
- },
- });
- };
-
- describe.each([
- { message: '' },
- { message: 'foo' },
- groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }),
- groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }),
- groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }),
- ])('given the message %p', (message) => {
- beforeEach(() => {
- createWrapper(message);
- });
-
- it('interpolates correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
index 919abc26e05..1154c930e5d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -40,8 +40,6 @@ describe('Chunk component', () => {
describe('rendering', () => {
it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
- jest.clearAllMocks();
-
expect(window.requestIdleCallback).not.toHaveBeenCalled();
expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
index d2dd4afe09e..49e3083f8ed 100644
--- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -1,10 +1,11 @@
-import hljs from 'highlight.js';
+import hljs from 'highlight.js/lib/core';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/constants';
-jest.mock('highlight.js', () => ({
+jest.mock('highlight.js/lib/core', () => ({
highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }),
+ registerLanguage: jest.fn(),
}));
jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
@@ -14,11 +15,15 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
const fileType = 'text';
const rawContent = 'function test() { return true }; \n // newline';
const highlightedContent = 'highlighted content';
-const language = 'javascript';
+const language = 'json';
describe('Highlight utility', () => {
beforeEach(() => highlight(fileType, rawContent, language));
+ it('registers the language', () => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
+ });
+
it('registers the plugins', () => {
expect(registerPlugins).toHaveBeenCalled();
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
index 9d2bf002d73..45fef09aa84 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -8,14 +8,15 @@ describe('Highlight.js plugin for wrapping _emitter nodes', () => {
children: [
{ scope: 'string', children: ['Text 1'] },
{ scope: 'string', children: ['Text 2', { scope: 'comment', children: ['Text 3'] }] },
- { scope: undefined, sublanguage: true, children: ['Text 3 (sublanguage)'] },
- 'Text4\nText5',
+ { scope: undefined, sublanguage: true, children: ['Text 4 (sublanguage)'] },
+ { scope: undefined, sublanguage: undefined, children: ['Text 5'] },
+ 'Text6\nText7',
],
},
},
};
- const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 3 (sublanguage)</span><span class="">Text4</span>\n<span class="">Text5</span>`;
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 4 (sublanguage)</span><span class="">Text 5</span><span class="">Text6</span>\n<span class="">Text7</span>`;
wrapChildNodes(hljsResultMock);
expect(hljsResultMock.value).toBe(outputValue);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js
new file mode 100644
index 00000000000..def76856dba
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js
@@ -0,0 +1,12 @@
+import wrapLines from '~/vue_shared/components/source_viewer/plugins/wrap_lines';
+
+describe('Highlight.js plugin for wrapping lines', () => {
+ it('mutates the input value by wrapping each line in a div with the correct attributes', () => {
+ const inputValue = `// some content`;
+ const outputValue = `<div id="LC1" lang="javascript" class="line">${inputValue}</div>`;
+ const hljsResultMock = { value: inputValue, language: 'javascript' };
+
+ wrapLines(hljsResultMock);
+ expect(hljsResultMock.value).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
index 715234e56fd..6b711b6b6b2 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -3,9 +3,11 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_ne
import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
+import LineHighlighter from '~/blob/line_highlighter';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
+jest.mock('~/blob/line_highlighter');
jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
@@ -25,6 +27,10 @@ describe('Source Viewer component', () => {
return createComponent();
});
+ it('instantiates the lineHighlighter class', () => {
+ expect(LineHighlighter).toHaveBeenCalled();
+ });
+
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/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index 24f96195e05..776395b9717 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
@@ -1,6 +1,6 @@
import { GlIcon, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
describe('Upload dropzone component', () => {
@@ -11,13 +11,13 @@ describe('Upload dropzone component', () => {
};
const findDropzoneCard = () => wrapper.find('.upload-dropzone-card');
- const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
+ const findDropzoneArea = () => wrapper.findByTestId('dropzone-area');
const findIcon = () => wrapper.findComponent(GlIcon);
- const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
+ const findUploadText = () => wrapper.findByTestId('upload-text').text();
const findFileInput = () => wrapper.find('input[type="file"]');
- function createComponent({ slots = {}, data = {}, props = {} } = {}) {
- wrapper = shallowMount(UploadDropzone, {
+ function createComponent({ slots = {}, props = {} } = {}) {
+ wrapper = shallowMountExtended(UploadDropzone, {
slots,
propsData: {
displayAsCard: true,
@@ -26,9 +26,6 @@ describe('Upload dropzone component', () => {
stubs: {
GlSprintf,
},
- data() {
- return data;
- },
});
}
@@ -112,53 +109,50 @@ describe('Upload dropzone component', () => {
wrapper.trigger('drop', mockEvent);
await nextTick();
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ expect(wrapper.emitted('change')).toEqual([[[mockFile]]]);
});
});
describe('ondrop', () => {
- const mockData = { dragCounter: 1, isDragDataValid: true };
-
describe('when drag data is valid', () => {
it('emits upload event for valid files', () => {
- createComponent({ data: mockData });
+ createComponent();
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
- wrapper.vm.ondrop(mockEvent);
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ wrapper.trigger('drop', mockEvent);
+ expect(wrapper.emitted('change')).toEqual([[[mockFile]]]);
});
it('emits error event when files are invalid', () => {
- createComponent({ data: mockData });
+ createComponent();
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
- wrapper.vm.ondrop(mockEvent);
+ wrapper.trigger('drop', mockEvent);
expect(wrapper.emitted()).toHaveProperty('error');
});
it('allows validation function to be overwritten', () => {
- createComponent({ data: mockData, props: { isFileValid: () => true } });
+ createComponent({ props: { isFileValid: () => true } });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
- wrapper.vm.ondrop(mockEvent);
+ wrapper.trigger('drop', mockEvent);
expect(wrapper.emitted()).not.toHaveProperty('error');
});
describe('singleFileSelection = true', () => {
it('emits a single file on drop', () => {
createComponent({
- data: mockData,
props: { singleFileSelection: true },
});
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
- wrapper.vm.ondrop(mockEvent);
- expect(wrapper.emitted().change[0]).toEqual([mockFile]);
+ wrapper.trigger('drop', mockEvent);
+ expect(wrapper.emitted('change')).toEqual([[mockFile]]);
});
});
});
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 90f9156af38..443d4e32580 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
@@ -95,4 +95,18 @@ describe('User Avatar Link Component', () => {
expect(wrapper.html()).toContain(badge);
});
});
+
+ describe('when popover props provided', () => {
+ beforeEach(() => {
+ createWrapper({ popoverUserId: 1, popoverUsername: defaultProps.username });
+ });
+
+ it('should render GlAvatarLink with popover support', () => {
+ expect(wrapper.attributes()).toMatchObject({
+ href: defaultProps.linkHref,
+ 'data-user-id': '1',
+ 'data-username': `${defaultProps.username}`,
+ });
+ });
+ });
});
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 075cb753301..32f9df8a63c 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
@@ -14,6 +14,7 @@ const DEFAULT_EMPTY_MESSAGE = 'None';
const createUser = (id) => ({
id,
name: 'Lorem',
+ username: 'lorem.ipsum',
web_url: `${TEST_HOST}/${id}`,
avatar_url: `${TEST_HOST}/${id}/avatar`,
});
@@ -90,6 +91,8 @@ describe('UserAvatarList', () => {
imgAlt: x.name,
tooltipText: x.name,
imgSize: TEST_IMAGE_SIZE,
+ popoverUserId: x.id,
+ popoverUsername: x.username,
}),
),
);
@@ -107,6 +110,8 @@ describe('UserAvatarList', () => {
imgAlt: x.name,
tooltipText: x.name,
imgSize: TEST_IMAGE_SIZE,
+ popoverUserId: x.id,
+ popoverUsername: x.username,
}),
),
);
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 41181ab9a68..0457044f985 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
@@ -31,6 +31,7 @@ const DEFAULT_PROPS = {
name: 'Administrator',
location: 'Vienna',
localTime: '2:30 PM',
+ webUrl: '/root',
bot: false,
bio: null,
workInformation: null,
@@ -71,11 +72,11 @@ describe('User Popover Component', () => {
});
};
- const createWrapper = (props = {}) => {
+ const createWrapper = (props = {}, target = findTarget()) => {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
- target: findTarget(),
+ target,
...props,
},
});
@@ -518,4 +519,35 @@ describe('User Popover Component', () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
+
+ describe('when current user is assignee/reviewer in a Merge Request', () => {
+ const { id, username, webUrl } = DEFAULT_PROPS.user;
+ const target = document.createElement('a');
+ target.setAttribute('href', webUrl);
+ target.classList.add('js-user-link');
+ target.dataset.currentUserId = id;
+ target.dataset.currentUsername = username;
+
+ it('renders popover with warning when user unable to merge', () => {
+ target.dataset.cannotMerge = 'true';
+
+ createWrapper({}, target);
+
+ const cannotMergeWarning = wrapper.findByTestId('cannot-merge');
+
+ expect(cannotMergeWarning.exists()).toBe(true);
+ expect(cannotMergeWarning.text()).toContain('Cannot merge');
+ expect(cannotMergeWarning.findComponent(GlIcon).props('name')).toBe('warning-solid');
+ });
+
+ it('renders popover without any warning when user is able to merge', () => {
+ delete target.dataset.cannotMerge;
+
+ createWrapper({}, target);
+
+ const cannotMergeWarning = wrapper.findByTestId('cannot-merge');
+
+ expect(cannotMergeWarning.exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index e881bfed35e..8c7657da8bc 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -1,10 +1,10 @@
import { GlSearchBoxByType } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
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 { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
@@ -44,20 +44,20 @@ Vue.use(VueApollo);
describe('User select dropdown', () => {
let wrapper;
let fakeApollo;
+ const hideDropdownMock = jest.fn();
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
- const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
- const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
+ const findParticipantsLoading = () => wrapper.findByTestId('loading-participants');
+ const findSelectedParticipants = () => wrapper.findAllByTestId('selected-participant');
const findSelectedParticipantByIndex = (index) =>
findSelectedParticipants().at(index).findComponent(SidebarParticipant);
- const findUnselectedParticipants = () =>
- wrapper.findAll('[data-testid="unselected-participant"]');
+ const findUnselectedParticipants = () => wrapper.findAllByTestId('unselected-participant');
const findUnselectedParticipantByIndex = (index) =>
findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
- const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
- const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
- const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
- const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
+ const findCurrentUser = () => wrapper.findAllByTestId('current-user');
+ const findIssuableAuthor = () => wrapper.findAllByTestId('issuable-author');
+ const findUnassignLink = () => wrapper.findByTestId('unassign');
+ const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
@@ -72,7 +72,7 @@ describe('User select dropdown', () => {
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
[getIssueParticipantsQuery, participantsQueryHandler],
]);
- wrapper = shallowMount(UserSelect, {
+ wrapper = shallowMountExtended(UserSelect, {
apolloProvider: fakeApollo,
propsData: {
headerText: 'test',
@@ -97,7 +97,7 @@ describe('User select dropdown', () => {
</div>
`,
methods: {
- hide: jest.fn(),
+ hide: hideDropdownMock,
},
},
},
@@ -106,6 +106,7 @@ describe('User select dropdown', () => {
afterEach(() => {
fakeApollo = null;
+ hideDropdownMock.mockClear();
});
it('renders a loading spinner if participants are loading', () => {
@@ -290,12 +291,12 @@ describe('User select dropdown', () => {
value: [assignee],
},
});
- wrapper.vm.$refs.dropdown.hide = jest.fn();
+
await waitForPromises();
findUnassignLink().trigger('click');
- expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ expect(hideDropdownMock).toHaveBeenCalledTimes(1);
});
it('emits an empty array after unselecting the only selected assignee', async () => {
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 e54de25dc0d..b6c22ceaa23 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -85,7 +85,7 @@ describe('vue_shared/components/web_ide_link', () => {
let wrapper;
- function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) {
+ function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
const fakeApollo = createMockApollo([
[getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)],
]);
@@ -98,9 +98,7 @@ describe('vue_shared/components/web_ide_link', () => {
forkPath,
...props,
},
- provide: {
- glFeatures,
- },
+ slots,
stubs: {
GlModal: stubComponent(GlModal, {
template: `
@@ -215,6 +213,27 @@ describe('vue_shared/components/web_ide_link', () => {
expect(findActionsButton().props('actions')).toEqual(expectedActions);
});
+ it('bubbles up shown and hidden events triggered by actions button component', () => {
+ createComponent();
+
+ expect(wrapper.emitted('shown')).toBe(undefined);
+ expect(wrapper.emitted('hidden')).toBe(undefined);
+
+ findActionsButton().vm.$emit('shown');
+ findActionsButton().vm.$emit('hidden');
+
+ expect(wrapper.emitted('shown')).toHaveLength(1);
+ expect(wrapper.emitted('hidden')).toHaveLength(1);
+ });
+
+ it('exposes a default slot', () => {
+ const slotContent = 'default slot content';
+
+ createComponent({}, { slots: { default: slotContent } });
+
+ expect(wrapper.text()).toContain(slotContent);
+ });
+
describe('when pipeline editor action is available', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index 964b48f4275..f8cf3ba5271 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -5,6 +5,7 @@ import {
} from 'jest/vue_shared/components/filtered_search_bar/mock_data';
export const mockAuthor = {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
name: 'Administrator',
@@ -13,6 +14,7 @@ export const mockAuthor = {
};
export const mockRegularLabel = {
+ __typename: 'Label',
id: 'gid://gitlab/GroupLabel/2048',
title: 'Documentation Update',
description: null,
@@ -21,6 +23,7 @@ export const mockRegularLabel = {
};
export const mockScopedLabel = {
+ __typename: 'Label',
id: 'gid://gitlab/ProjectLabel/2049',
title: 'status::confirmed',
description: null,
@@ -31,6 +34,7 @@ export const mockScopedLabel = {
export const mockLabels = [mockRegularLabel, mockScopedLabel];
export const mockCurrentUserTodo = {
+ __typename: 'Todo',
id: 'gid://gitlab/Todo/489',
state: 'done',
};
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 fa38ab8d44d..d2b7b2e89c8 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
@@ -1,13 +1,16 @@
import { GlButton, GlBadge, GlIcon, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
-
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
const issuableHeaderProps = {
...mockIssuable,
...mockIssuableShowProps,
+ issuableType: TYPE_ISSUE,
+ workspaceType: WORKSPACE_PROJECT,
};
describe('IssuableHeader', () => {
@@ -53,6 +56,14 @@ describe('IssuableHeader', () => {
setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
});
+ it('emits a "toggle" event', () => {
+ createComponent();
+
+ findButton().vm.$emit('click');
+
+ expect(wrapper.emitted('toggle')).toEqual([[]]);
+ });
+
it('dispatches `click` event on sidebar toggle button', () => {
createComponent();
const toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
@@ -94,14 +105,12 @@ describe('IssuableHeader', () => {
});
it('renders confidential icon when issuable is confidential', () => {
- createComponent({
- confidential: true,
- });
+ createComponent({ confidential: true });
- const confidentialEl = wrapper.findByTestId('confidential');
-
- expect(confidentialEl.exists()).toBe(true);
- expect(confidentialEl.findComponent(GlIcon).props('name')).toBe('eye-slash');
+ expect(wrapper.findComponent(ConfidentialityBadge).props()).toEqual({
+ issuableType: 'issue',
+ workspaceType: 'project',
+ });
});
it('renders issuable author avatar', () => {
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 cc8a8d86d19..3306e316ed0 100644
--- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
@@ -39,6 +39,18 @@ describe('Welcome page', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
});
+ it('renders image', () => {
+ const mockImgSrc = 'image1.svg';
+
+ createComponent({
+ propsData: {
+ panels: [{ name: 'test', href: '#', imageSrc: mockImgSrc }],
+ },
+ });
+
+ expect(wrapper.find('img').attributes('src')).toBe(mockImgSrc);
+ });
+
it('renders footer slot if provided', () => {
const DUMMY = 'Test message';
createComponent({
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 abc69da7a58..a7ddcbdd8bc 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
@@ -15,6 +15,7 @@ describe('Experimental new namespace creation app', () => {
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
+ const findImage = () => wrapper.find('img');
const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
const findSuperSidebarToggle = () => wrapper.findComponent(SuperSidebarToggle);
@@ -22,8 +23,8 @@ describe('Experimental new namespace creation app', () => {
title: 'Create something',
initialBreadcrumbs: [{ text: 'Something', href: '#' }],
panels: [
- { name: 'panel1', selector: '#some-selector1' },
- { name: 'panel2', selector: '#some-selector2' },
+ { name: 'panel1', selector: '#some-selector1', imageSrc: 'panel1.svg' },
+ { name: 'panel2', selector: '#some-selector2', imageSrc: 'panel2.svg' },
],
persistenceKey: 'DEMO-PERSISTENCE-KEY',
};
@@ -82,6 +83,10 @@ describe('Experimental new namespace creation app', () => {
expect(breadcrumb.exists()).toBe(true);
expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumbs[0].text);
});
+
+ it('renders images', () => {
+ expect(findImage().attributes('src')).toBe(DEFAULT_PROPS.panels[1].imageSrc);
+ });
});
it('renders extra description if provided', () => {
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index a9ad675e538..533d312a4de 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -341,120 +341,6 @@ export const securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse = {
},
};
-export const securityReportMergeRequestDownloadPathsQueryResponse = {
- project: {
- id: '1',
- mergeRequest: {
- id: 'mr-1',
- headPipeline: {
- id: 'gid://gitlab/Ci::Pipeline/176',
- jobs: {
- nodes: [
- {
- id: 'job-1',
- name: 'secret_detection',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
- fileType: 'SECRET_DETECTION',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- {
- id: 'job-2',
- name: 'bandit-sast',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
- fileType: 'SAST',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- {
- id: 'job-3',
- name: 'eslint-sast',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
- fileType: 'SAST',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- {
- id: 'job-4',
- name: 'all_artifacts',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive',
- fileType: 'ARCHIVE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata',
- fileType: 'METADATA',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- ],
- __typename: 'CiJobConnection',
- },
- __typename: 'Pipeline',
- },
- __typename: 'MergeRequest',
- },
- __typename: 'Project',
- },
-};
-
export const securityReportPipelineDownloadPathsQueryResponse = {
project: {
id: 'project-1',
@@ -566,9 +452,6 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
__typename: 'Project',
};
-/**
- * These correspond to SAST jobs in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const sastArtifacts = [
{
name: 'bandit-sast',
@@ -582,9 +465,6 @@ export const sastArtifacts = [
},
];
-/**
- * These correspond to Secret Detection jobs in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const secretDetectionArtifacts = [
{
name: 'secret_detection',
@@ -594,13 +474,6 @@ export const secretDetectionArtifacts = [
},
];
-export const expectedDownloadDropdownPropsWithTitle = {
- loading: false,
- artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
- text: '',
- title: 'Download results',
-};
-
export const expectedDownloadDropdownPropsWithText = {
loading: false,
artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
@@ -608,9 +481,6 @@ export const expectedDownloadDropdownPropsWithText = {
text: 'Download results',
};
-/**
- * These correspond to any jobs with zip archives in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const archiveArtifacts = [
{
name: 'all_artifacts Archive',
@@ -619,9 +489,6 @@ export const archiveArtifacts = [
},
];
-/**
- * These correspond to any jobs with trace data in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const traceArtifacts = [
{
name: 'secret_detection Trace',
@@ -645,9 +512,6 @@ export const traceArtifacts = [
},
];
-/**
- * These correspond to any jobs with metadata data in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const metadataArtifacts = [
{
name: 'all_artifacts Metadata',
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
deleted file mode 100644
index 257f59612e8..00000000000
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ /dev/null
@@ -1,267 +0,0 @@
-import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { merge } from 'lodash';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import Vuex from 'vuex';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { trimText } from 'helpers/text_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import {
- expectedDownloadDropdownPropsWithText,
- securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse,
- securityReportMergeRequestDownloadPathsQueryResponse,
- sastDiffSuccessMock,
- secretDetectionDiffSuccessMock,
-} from 'jest/vue_shared/security_reports/mock_data';
-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';
-import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
-import {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SECRET_DETECTION,
-} from '~/vue_shared/security_reports/constants';
-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('~/alert');
-
-Vue.use(VueApollo);
-Vue.use(Vuex);
-
-const SAST_COMPARISON_PATH = '/sast.json';
-const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json';
-
-describe('Security reports app', () => {
- let wrapper;
-
- const props = {
- pipelineId: 123,
- projectId: 456,
- securityReportsDocsPath: '/docs',
- discoverProjectSecurityPath: '/discoverProjectSecurityPath',
- };
-
- const createComponent = (options) => {
- wrapper = mount(
- SecurityReportsApp,
- merge(
- {
- propsData: { ...props },
- stubs: {
- HelpIcon: true,
- },
- },
- options,
- ),
- );
- };
-
- const pendingHandler = () => new Promise(() => {});
- const successHandler = () =>
- Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
- const successEmptyHandler = () =>
- Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse });
- const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
- const createMockApolloProvider = (handler) => {
- const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
-
- return createMockApollo(requestHandlers);
- };
-
- const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
- const findHelpIconComponent = () => wrapper.findComponent(HelpIcon);
-
- describe('given the artifacts query is loading', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(pendingHandler),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('initially renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the artifacts query loads successfully', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(successHandler),
- });
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
-
- it('renders the expected message', () => {
- expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
- });
-
- it('renders a help link', () => {
- expect(findHelpIconComponent().props()).toEqual({
- helpPath: props.securityReportsDocsPath,
- discoverProjectSecurityPath: props.discoverProjectSecurityPath,
- });
- });
- });
-
- describe('given the artifacts query loads successfully with no artifacts', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(successEmptyHandler),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('initially renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the artifacts query fails', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(failureHandler),
- });
- });
-
- it('calls createAlert correctly', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error: expect.any(Error),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
- let mock;
-
- const createComponentWithFlagEnabled = (options) =>
- createComponent(
- merge(options, {
- provide: {
- glFeatures: {
- coreSecurityMrWidgetCounts: true,
- },
- },
- apolloProvider: createMockApolloProvider(successHandler),
- }),
- );
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- const SAST_SUCCESS_MESSAGE =
- 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
- const SECRET_DETECTION_SUCCESS_MESSAGE =
- 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
- describe.each`
- reportType | pathProp | path | successResponse | successMessage
- ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
- ${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE}
- `(
- 'given a $pathProp and $reportType artifact',
- ({ pathProp, path, successResponse, successMessage }) => {
- describe('when loading', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios, { delayResponse: 1 });
- mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
-
- createComponentWithFlagEnabled({
- propsData: {
- [pathProp]: path,
- },
- });
-
- return waitForPromises();
- });
-
- it('should have loading message', () => {
- expect(wrapper.text()).toContain('Security scanning is loading');
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
- });
-
- describe('when successfully loaded', () => {
- beforeEach(() => {
- mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
-
- createComponentWithFlagEnabled({
- propsData: {
- [pathProp]: path,
- },
- });
-
- return waitForPromises();
- });
-
- it('should show counts', () => {
- expect(trimText(wrapper.text())).toContain(successMessage);
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
- });
-
- describe('when an error occurs', () => {
- beforeEach(() => {
- mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- createComponentWithFlagEnabled({
- propsData: {
- [pathProp]: path,
- },
- });
-
- return waitForPromises();
- });
-
- it('should show error message', () => {
- expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
- });
-
- describe('when the comparison endpoint is not provided', () => {
- beforeEach(() => {
- mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- createComponentWithFlagEnabled();
-
- return waitForPromises();
- });
-
- it('renders the basic scansHaveRun message', () => {
- expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
- });
- });
- },
- );
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
deleted file mode 100644
index bcc8955ba02..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import {
- groupedSummaryText,
- allReportsHaveError,
- areReportsLoading,
- anyReportHasError,
- areAllReportsLoading,
- anyReportHasIssues,
- summaryCounts,
-} from '~/vue_shared/security_reports/store/getters';
-import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
-import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-import createState from '~/vue_shared/security_reports/store/state';
-import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
-import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
-
-const generateVuln = (severity) => ({ severity });
-
-describe('Security reports getters', () => {
- let state;
-
- beforeEach(() => {
- state = createState();
- state.sast = createSastState();
- state.secretDetection = createSecretDetectionState();
- });
-
- describe('summaryCounts', () => {
- it('returns 0 count for empty state', () => {
- expect(summaryCounts(state)).toEqual({
- critical: 0,
- high: 0,
- other: 0,
- });
- });
-
- describe('combines all reports', () => {
- it('of the same severity', () => {
- state.sast.newIssues = [generateVuln(CRITICAL)];
- state.secretDetection.newIssues = [generateVuln(CRITICAL)];
-
- expect(summaryCounts(state)).toEqual({
- critical: 2,
- high: 0,
- other: 0,
- });
- });
-
- it('of different severities', () => {
- state.sast.newIssues = [generateVuln(CRITICAL)];
- state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)];
-
- expect(summaryCounts(state)).toEqual({
- critical: 1,
- high: 1,
- other: 1,
- });
- });
- });
- });
-
- describe('groupedSummaryText', () => {
- it('returns failed text', () => {
- expect(
- groupedSummaryText(state, {
- allReportsHaveError: true,
- areReportsLoading: false,
- summaryCounts: {},
- }),
- ).toEqual({ message: 'Security scanning failed loading any results' });
- });
-
- it('returns `is loading` as status text', () => {
- expect(
- groupedSummaryText(state, {
- allReportsHaveError: false,
- areReportsLoading: true,
- summaryCounts: {},
- }),
- ).toEqual(
- groupedTextBuilder({
- reportType: 'Security scanning',
- critical: 0,
- high: 0,
- other: 0,
- status: 'is loading',
- }),
- );
- });
-
- it('returns no new status text if there are existing ones', () => {
- expect(
- groupedSummaryText(state, {
- allReportsHaveError: false,
- areReportsLoading: false,
- summaryCounts: {},
- }),
- ).toEqual(
- groupedTextBuilder({
- reportType: 'Security scanning',
- critical: 0,
- high: 0,
- other: 0,
- status: '',
- }),
- );
- });
- });
-
- describe('areReportsLoading', () => {
- it('returns true when any report is loading', () => {
- state.sast.isLoading = true;
-
- expect(areReportsLoading(state)).toEqual(true);
- });
-
- it('returns false when none of the reports are loading', () => {
- expect(areReportsLoading(state)).toEqual(false);
- });
- });
-
- describe('areAllReportsLoading', () => {
- it('returns true when all reports are loading', () => {
- state.sast.isLoading = true;
- state.secretDetection.isLoading = true;
-
- expect(areAllReportsLoading(state)).toEqual(true);
- });
-
- it('returns false when some of the reports are loading', () => {
- state.sast.isLoading = true;
-
- expect(areAllReportsLoading(state)).toEqual(false);
- });
-
- it('returns false when none of the reports are loading', () => {
- expect(areAllReportsLoading(state)).toEqual(false);
- });
- });
-
- describe('allReportsHaveError', () => {
- it('returns true when all reports have error', () => {
- state.sast.hasError = true;
- state.secretDetection.hasError = true;
-
- expect(allReportsHaveError(state)).toEqual(true);
- });
-
- it('returns false when none of the reports have error', () => {
- expect(allReportsHaveError(state)).toEqual(false);
- });
-
- it('returns false when one of the reports does not have error', () => {
- state.secretDetection.hasError = true;
-
- expect(allReportsHaveError(state)).toEqual(false);
- });
- });
-
- describe('anyReportHasError', () => {
- it('returns true when any of the reports has error', () => {
- state.sast.hasError = true;
-
- expect(anyReportHasError(state)).toEqual(true);
- });
-
- it('returns false when none of the reports has error', () => {
- expect(anyReportHasError(state)).toEqual(false);
- });
- });
-
- describe('anyReportHasIssues', () => {
- it('returns true when any of the reports has new issues', () => {
- state.sast.newIssues.push(generateVuln(LOW));
-
- expect(anyReportHasIssues(state)).toEqual(true);
- });
-
- it('returns false when none of the reports has error', () => {
- expect(anyReportHasIssues(state)).toEqual(false);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
deleted file mode 100644
index 0cab950cb77..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
+++ /dev/null
@@ -1,197 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
-import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
-import createState from '~/vue_shared/security_reports/store/modules/sast/state';
-
-const diffEndpoint = 'diff-endpoint.json';
-const blobPath = 'blob-path.json';
-const reports = {
- base: 'base',
- head: 'head',
- enrichData: 'enrichData',
- diff: 'diff',
-};
-const error = 'Something went wrong';
-const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
-const rootState = { vulnerabilityFeedbackPath, blobPath };
-
-let state;
-
-describe('sast report actions', () => {
- beforeEach(() => {
- state = createState();
- });
-
- describe('setDiffEndpoint', () => {
- it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
- return testAction(
- actions.setDiffEndpoint,
- diffEndpoint,
- state,
- [
- {
- type: types.SET_DIFF_ENDPOINT,
- payload: diffEndpoint,
- },
- ],
- [],
- );
- });
- });
-
- describe('requestDiff', () => {
- it(`should commit ${types.REQUEST_DIFF}`, () => {
- return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
- });
- });
-
- describe('receiveDiffSuccess', () => {
- it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
- return testAction(
- actions.receiveDiffSuccess,
- reports,
- state,
- [
- {
- type: types.RECEIVE_DIFF_SUCCESS,
- payload: reports,
- },
- ],
- [],
- );
- });
- });
-
- describe('receiveDiffError', () => {
- it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
- return testAction(
- actions.receiveDiffError,
- error,
- state,
- [
- {
- type: types.RECEIVE_DIFF_ERROR,
- payload: error,
- },
- ],
- [],
- );
- });
- });
-
- describe('fetchDiff', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state.paths.diffEndpoint = diffEndpoint;
- rootState.canReadVulnerabilityFeedback = true;
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('when diff and vulnerability feedback endpoints respond successfully', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffSuccess` action', () => {
- const { diff, enrichData } = reports;
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
- beforeEach(() => {
- rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
- });
-
- it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
- const { diff } = reports;
- const enrichData = [];
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when the vulnerability feedback endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_NOT_FOUND);
- });
-
- it('should dispatch the `receiveError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
-
- describe('when the diff endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_NOT_FOUND)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
deleted file mode 100644
index d6119f44619..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
-import mutations from '~/vue_shared/security_reports/store/modules/sast/mutations';
-import createState from '~/vue_shared/security_reports/store/modules/sast/state';
-
-const createIssue = ({ ...config }) => ({ changed: false, ...config });
-
-describe('sast module mutations', () => {
- const path = 'path';
- let state;
-
- beforeEach(() => {
- state = createState();
- });
-
- describe(types.SET_DIFF_ENDPOINT, () => {
- it('should set the SAST diff endpoint', () => {
- mutations[types.SET_DIFF_ENDPOINT](state, path);
-
- expect(state.paths.diffEndpoint).toBe(path);
- });
- });
-
- describe(types.REQUEST_DIFF, () => {
- it('should set the `isLoading` status to `true`', () => {
- mutations[types.REQUEST_DIFF](state);
-
- expect(state.isLoading).toBe(true);
- });
- });
-
- describe(types.RECEIVE_DIFF_SUCCESS, () => {
- beforeEach(() => {
- const reports = {
- diff: {
- added: [
- createIssue({ cve: 'CVE-1' }),
- createIssue({ cve: 'CVE-2' }),
- createIssue({ cve: 'CVE-3' }),
- ],
- fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
- existing: [createIssue({ cve: 'CVE-6' })],
- base_report_out_of_date: true,
- },
- };
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `baseReportOutofDate` status to `false`', () => {
- expect(state.baseReportOutofDate).toBe(true);
- });
-
- it('should have the relevant `new` issues', () => {
- expect(state.newIssues).toHaveLength(3);
- });
-
- it('should have the relevant `resolved` issues', () => {
- expect(state.resolvedIssues).toHaveLength(2);
- });
-
- it('should have the relevant `all` issues', () => {
- expect(state.allIssues).toHaveLength(1);
- });
- });
-
- describe(types.RECEIVE_DIFF_ERROR, () => {
- beforeEach(() => {
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_ERROR](state);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `hasError` status to `true`', () => {
- expect(state.hasError).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
deleted file mode 100644
index 7197784c3e8..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
+++ /dev/null
@@ -1,198 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
-import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
-import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-
-const diffEndpoint = 'diff-endpoint.json';
-const blobPath = 'blob-path.json';
-const reports = {
- base: 'base',
- head: 'head',
- enrichData: 'enrichData',
- diff: 'diff',
-};
-const error = 'Something went wrong';
-const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
-const rootState = { vulnerabilityFeedbackPath, blobPath };
-
-let state;
-
-describe('secret detection report actions', () => {
- beforeEach(() => {
- state = createState();
- });
-
- describe('setDiffEndpoint', () => {
- it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
- return testAction(
- actions.setDiffEndpoint,
- diffEndpoint,
- state,
- [
- {
- type: types.SET_DIFF_ENDPOINT,
- payload: diffEndpoint,
- },
- ],
- [],
- );
- });
- });
-
- describe('requestDiff', () => {
- it(`should commit ${types.REQUEST_DIFF}`, () => {
- return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
- });
- });
-
- describe('receiveDiffSuccess', () => {
- it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
- return testAction(
- actions.receiveDiffSuccess,
- reports,
- state,
- [
- {
- type: types.RECEIVE_DIFF_SUCCESS,
- payload: reports,
- },
- ],
- [],
- );
- });
- });
-
- describe('receiveDiffError', () => {
- it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
- return testAction(
- actions.receiveDiffError,
- error,
- state,
- [
- {
- type: types.RECEIVE_DIFF_ERROR,
- payload: error,
- },
- ],
- [],
- );
- });
- });
-
- describe('fetchDiff', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state.paths.diffEndpoint = diffEndpoint;
- rootState.canReadVulnerabilityFeedback = true;
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('when diff and vulnerability feedback endpoints respond successfully', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffSuccess` action', () => {
- const { diff, enrichData } = reports;
-
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
- beforeEach(() => {
- rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
- });
-
- it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
- const { diff } = reports;
- const enrichData = [];
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when the vulnerability feedback endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_NOT_FOUND);
- });
-
- it('should dispatch the `receiveDiffError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
-
- describe('when the diff endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_NOT_FOUND)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
deleted file mode 100644
index 42da7476a40..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
-import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations';
-import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-
-const createIssue = ({ ...config }) => ({ changed: false, ...config });
-
-describe('secret detection module mutations', () => {
- const path = 'path';
- let state;
-
- beforeEach(() => {
- state = createState();
- });
-
- describe(types.SET_DIFF_ENDPOINT, () => {
- it('should set the secret detection diff endpoint', () => {
- mutations[types.SET_DIFF_ENDPOINT](state, path);
-
- expect(state.paths.diffEndpoint).toBe(path);
- });
- });
-
- describe(types.REQUEST_DIFF, () => {
- it('should set the `isLoading` status to `true`', () => {
- mutations[types.REQUEST_DIFF](state);
-
- expect(state.isLoading).toBe(true);
- });
- });
-
- describe(types.RECEIVE_DIFF_SUCCESS, () => {
- beforeEach(() => {
- const reports = {
- diff: {
- added: [
- createIssue({ cve: 'CVE-1' }),
- createIssue({ cve: 'CVE-2' }),
- createIssue({ cve: 'CVE-3' }),
- ],
- fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
- existing: [createIssue({ cve: 'CVE-6' })],
- base_report_out_of_date: true,
- },
- };
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `baseReportOutofDate` status to `true`', () => {
- expect(state.baseReportOutofDate).toBe(true);
- });
-
- it('should have the relevant `new` issues', () => {
- expect(state.newIssues).toHaveLength(3);
- });
-
- it('should have the relevant `resolved` issues', () => {
- expect(state.resolvedIssues).toHaveLength(2);
- });
-
- it('should have the relevant `all` issues', () => {
- expect(state.allIssues).toHaveLength(1);
- });
- });
-
- describe(types.RECEIVE_DIFF_ERROR, () => {
- beforeEach(() => {
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_ERROR](state);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `hasError` status to `true`', () => {
- expect(state.hasError).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/utils_spec.js b/spec/frontend/vue_shared/security_reports/store/utils_spec.js
deleted file mode 100644
index c8750cd58a0..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/utils_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils';
-import {
- FEEDBACK_TYPE_DISMISSAL,
- FEEDBACK_TYPE_ISSUE,
- FEEDBACK_TYPE_MERGE_REQUEST,
-} from '~/vue_shared/security_reports/constants';
-
-describe('security reports store utils', () => {
- const vulnerability = { uuid: 1 };
-
- describe('enrichVulnerabilityWithFeedback', () => {
- const dismissalFeedback = {
- feedback_type: FEEDBACK_TYPE_DISMISSAL,
- finding_uuid: vulnerability.uuid,
- };
- const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback };
-
- const issueFeedback = {
- feedback_type: FEEDBACK_TYPE_ISSUE,
- issue_iid: 1,
- finding_uuid: vulnerability.uuid,
- };
- const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback };
- const mrFeedback = {
- feedback_type: FEEDBACK_TYPE_MERGE_REQUEST,
- merge_request_iid: 1,
- finding_uuid: vulnerability.uuid,
- };
- const mrVuln = {
- ...vulnerability,
- hasMergeRequest: true,
- merge_request_feedback: mrFeedback,
- };
-
- it.each`
- feedbacks | expected
- ${[dismissalFeedback]} | ${dismissalVuln}
- ${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability}
- ${[issueFeedback]} | ${issueVuln}
- ${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability}
- ${[mrFeedback]} | ${mrVuln}
- ${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }}
- `('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => {
- const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
-
- expect(enrichedVulnerability).toEqual(expected);
- });
-
- it('matches correct feedback objects to vulnerability', () => {
- const feedbacks = [
- dismissalFeedback,
- issueFeedback,
- mrFeedback,
- { ...dismissalFeedback, finding_uuid: 2 },
- { ...issueFeedback, finding_uuid: 2 },
- { ...mrFeedback, finding_uuid: 2 },
- ];
- const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
-
- expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/utils_spec.js b/spec/frontend/vue_shared/security_reports/utils_spec.js
deleted file mode 100644
index b7129ece698..00000000000
--- a/spec/frontend/vue_shared/security_reports/utils_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SECRET_DETECTION,
- REPORT_FILE_TYPES,
-} from '~/vue_shared/security_reports/constants';
-import {
- extractSecurityReportArtifactsFromMergeRequest,
- extractSecurityReportArtifactsFromPipeline,
-} from '~/vue_shared/security_reports/utils';
-import {
- securityReportMergeRequestDownloadPathsQueryResponse,
- securityReportPipelineDownloadPathsQueryResponse,
- sastArtifacts,
- secretDetectionArtifacts,
- archiveArtifacts,
- traceArtifacts,
- metadataArtifacts,
-} from './mock_data';
-
-describe.each([
- [
- 'extractSecurityReportArtifactsFromMergeRequest',
- extractSecurityReportArtifactsFromMergeRequest,
- securityReportMergeRequestDownloadPathsQueryResponse,
- ],
- [
- 'extractSecurityReportArtifactsFromPipelines',
- extractSecurityReportArtifactsFromPipeline,
- securityReportPipelineDownloadPathsQueryResponse,
- ],
-])('%s', (funcName, extractFunc, response) => {
- it.each`
- reportTypes | expectedArtifacts
- ${[]} | ${[]}
- ${['foo']} | ${[]}
- ${[REPORT_TYPE_SAST]} | ${sastArtifacts}
- ${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts}
- ${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]}
- ${[REPORT_FILE_TYPES.ARCHIVE]} | ${archiveArtifacts}
- ${[REPORT_FILE_TYPES.TRACE]} | ${traceArtifacts}
- ${[REPORT_FILE_TYPES.METADATA]} | ${metadataArtifacts}
- `(
- 'returns the expected artifacts given report types $reportTypes',
- ({ reportTypes, expectedArtifacts }) => {
- expect(extractFunc(reportTypes, response)).toEqual(expectedArtifacts);
- },
- );
-});
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 2e901783e07..e4180b2d178 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,11 +1,10 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
-import EmojiPicker from '~/emoji/components/picker.vue';
-import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
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';
@@ -15,18 +14,19 @@ Vue.use(VueApollo);
describe('Work Item Note Actions', () => {
let wrapper;
const noteId = '1';
+ const showSpy = jest.fn();
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 findEditButton = () => wrapper.findByTestId('edit-work-item-note');
+ const findEmojiButton = () => wrapper.findByTestId('note-emoji-button');
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
- const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
- const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
- const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]');
- const findAuthorBadge = () => wrapper.find('[data-testid="author-badge"]');
- const findMaxAccessLevelBadge = () => wrapper.find('[data-testid="max-access-level-badge"]');
- const findContributorBadge = () => wrapper.find('[data-testid="contributor-badge"]');
+ const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action');
+ const findCopyLinkButton = () => wrapper.findByTestId('copy-link-action');
+ const findAssignUnassignButton = () => wrapper.findByTestId('assign-note-action');
+ const findReportAbuseToAdminButton = () => wrapper.findByTestId('abuse-note-action');
+ const findAuthorBadge = () => wrapper.findByTestId('author-badge');
+ const findMaxAccessLevelBadge = () => wrapper.findByTestId('max-access-level-badge');
+ const findContributorBadge = () => wrapper.findByTestId('contributor-badge');
const addEmojiMutationResolver = jest.fn().mockResolvedValue({
data: {
@@ -34,11 +34,6 @@ describe('Work Item Note Actions', () => {
},
});
- const EmojiPickerStub = {
- props: EmojiPicker.props,
- template: '<div></div>',
- };
-
const createComponent = ({
showReply = true,
showEdit = true,
@@ -51,10 +46,12 @@ describe('Work Item Note Actions', () => {
maxAccessLevelOfAuthor = '',
projectName = 'Project name',
} = {}) => {
- wrapper = shallowMount(WorkItemNoteActions, {
+ wrapper = shallowMountExtended(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
+ workItemIid: '1',
+ note: {},
noteId,
showAwardEmoji,
showAssignUnassign,
@@ -66,21 +63,27 @@ describe('Work Item Note Actions', () => {
projectName,
},
provide: {
+ fullPath: 'gitlab-org',
glFeatures: {
workItemsMvc2: true,
},
},
stubs: {
- EmojiPicker: EmojiPickerStub,
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: { close: showSpy },
+ }),
},
apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]),
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
- wrapper.vm.$refs.dropdown.close = jest.fn();
};
+ afterEach(() => {
+ showSpy.mockClear();
+ });
+
describe('reply button', () => {
it('is visible by default', () => {
createComponent();
@@ -128,22 +131,6 @@ describe('Work Item Note Actions', () => {
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,
- });
- });
});
describe('delete note', () => {
@@ -173,6 +160,7 @@ describe('Work Item Note Actions', () => {
findDeleteNoteButton().vm.$emit('action');
expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
@@ -188,6 +176,7 @@ describe('Work Item Note Actions', () => {
findCopyLinkButton().vm.$emit('action');
expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
@@ -214,6 +203,7 @@ describe('Work Item Note Actions', () => {
findAssignUnassignButton().vm.$emit('action');
expect(wrapper.emitted('assignUser')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
@@ -240,6 +230,7 @@ describe('Work Item Note Actions', () => {
findReportAbuseToAdminButton().vm.$emit('action');
expect(wrapper.emitted('reportAbuse')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
new file mode 100644
index 00000000000..d425f1e50dc
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -0,0 +1,147 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import mockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { __ } from '~/locale';
+import AwardsList from '~/vue_shared/components/awards_list.vue';
+import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import {
+ mockWorkItemNotesResponseWithComments,
+ mockAwardEmojiThumbsUp,
+} from 'jest/work_items/mock_data';
+import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
+
+Vue.use(VueApollo);
+
+describe('Work Item Note Awards List', () => {
+ let wrapper;
+ const workItem = mockWorkItemNotesResponseWithComments.data.workspace.workItems.nodes[0];
+ const firstNote = workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes
+ .nodes[0];
+ const fullPath = 'test-project-path';
+ const workItemIid = workItem.iid;
+ const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsUp.user.id);
+
+ const addAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiAdd: {
+ errors: [],
+ },
+ },
+ });
+ const removeAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiRemove: {
+ errors: [],
+ },
+ },
+ });
+
+ const findAwardsList = () => wrapper.findComponent(AwardsList);
+
+ const createComponent = ({
+ note = firstNote,
+ addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler,
+ removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler,
+ } = {}) => {
+ const apolloProvider = mockApollo([
+ [addAwardEmojiMutation, addAwardEmojiMutationHandler],
+ [removeAwardEmojiMutation, removeAwardEmojiMutationHandler],
+ ]);
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ ...mockWorkItemNotesResponseWithComments,
+ });
+
+ wrapper = shallowMount(WorkItemNoteAwardsList, {
+ provide: {
+ fullPath,
+ },
+ propsData: {
+ workItemIid,
+ note,
+ isModal: false,
+ },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ window.gon.current_user_id = currentUserId;
+ });
+
+ describe('when not editing', () => {
+ it.each([true, false])('passes emoji permission to awards-list', (hasAwardEmojiPermission) => {
+ const note = {
+ ...firstNote,
+ userPermissions: {
+ ...firstNote.userPermissions,
+ awardEmoji: hasAwardEmojiPermission,
+ },
+ };
+ createComponent({ note });
+
+ expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission);
+ });
+
+ it('adds award if not already awarded', async () => {
+ createComponent();
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSUP,
+ });
+ });
+
+ it('emits error if awarding emoji fails', async () => {
+ createComponent({
+ addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
+ });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]);
+ });
+
+ it('removes award if already awarded', async () => {
+ const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
+
+ createComponent({ removeAwardEmojiMutationHandler });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+
+ await waitForPromises();
+
+ expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSDOWN,
+ });
+ });
+
+ it('restores award if remove fails', async () => {
+ createComponent({
+ removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
+ });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[__('Failed to remove emoji. Please try again')]]);
+ });
+ });
+});
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 8dbd2818fc5..c5d1decfb42 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
@@ -1,11 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { GlAvatarLink } from '@gitlab/ui';
import mockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
@@ -76,6 +78,7 @@ describe('Work Item Note', () => {
const errorHandler = jest.fn().mockRejectedValue('Oops');
+ const findAwardsList = () => wrapper.findComponent(WorkItemNoteAwardsList);
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
@@ -148,6 +151,13 @@ describe('Work Item Note', () => {
expect(findCommentForm().exists()).toBe(false);
expect(findNoteWrapper().exists()).toBe(true);
});
+
+ it('should show the awards list when in edit mode', async () => {
+ createComponent({ note: mockWorkItemCommentNote, workItemsMvc2: true });
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+ expect(findAwardsList().exists()).toBe(true);
+ });
});
describe('when submitting a form to edit a note', () => {
@@ -264,6 +274,19 @@ describe('Work Item Note', () => {
createComponent();
});
+ it('should show avatar link with popover support', () => {
+ const avatarLink = findTimelineEntryItem().findComponent(GlAvatarLink);
+ const { author } = mockWorkItemCommentNote;
+
+ expect(avatarLink.exists()).toBe(true);
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: author.webUrl,
+ 'data-user-id': '1',
+ 'data-username': `${author.username}`,
+ });
+ });
+
it('should have the note header, actions and body', () => {
expect(findTimelineEntryItem().exists()).toBe(true);
expect(findNoteHeader().exists()).toBe(true);
@@ -404,5 +427,12 @@ describe('Work Item Note', () => {
});
});
});
+
+ it('passes note props to awards list', () => {
+ createComponent({ note: mockWorkItemCommentNote, workItemsMvc2: true });
+
+ expect(findAwardsList().props('note')).toBe(mockWorkItemCommentNote);
+ expect(findAwardsList().props('workItemIid')).toBe('1');
+ });
});
});
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 94d47bfb3be..ff1998ab2ed 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -274,14 +274,14 @@ describe('WorkItemAssignees component', () => {
});
describe('when assigning to current user', () => {
- it('does not show `Assign myself` button if current user is loading', () => {
+ it('does not show `Assign yourself` button if current user is loading', () => {
createComponent();
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(false);
});
- it('does not show `Assign myself` button if work item has assignees', async () => {
+ it('does not show `Assign yourself` button if work item has assignees', async () => {
createComponent();
await waitForPromises();
findTokenSelector().trigger('mouseover');
@@ -289,7 +289,7 @@ describe('WorkItemAssignees component', () => {
expect(findAssignSelfButton().exists()).toBe(false);
});
- it('does now show `Assign myself` button if user is not logged in', async () => {
+ it('does now show `Assign yourself` button if user is not logged in', async () => {
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
await waitForPromises();
findTokenSelector().trigger('mouseover');
@@ -304,7 +304,7 @@ describe('WorkItemAssignees component', () => {
return waitForPromises();
});
- it('renders `Assign myself` button', () => {
+ it('renders `Assign yourself` button', () => {
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
new file mode 100644
index 00000000000..ba9af7b2b68
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
+import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
+import WorkItemState from '~/work_items/components/work_item_state.vue';
+import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
+import { workItemResponseFactory } from '../mock_data';
+
+describe('WorkItemAttributesWrapper component', () => {
+ let wrapper;
+
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+
+ const findWorkItemState = () => wrapper.findComponent(WorkItemState);
+ const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
+ const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
+ const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
+ const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
+
+ const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => {
+ wrapper = shallowMount(WorkItemAttributesWrapper, {
+ propsData: {
+ workItem,
+ },
+ provide: {
+ hasIssueWeightsFeature: true,
+ hasIterationsFeature: true,
+ hasOkrsFeature: true,
+ hasIssuableHealthStatusFeature: true,
+ projectNamespace: 'namespace',
+ fullPath: 'group/project',
+ },
+ stubs: {
+ WorkItemWeight: true,
+ WorkItemIteration: true,
+ WorkItemHealthStatus: true,
+ },
+ });
+ };
+
+ describe('work item state', () => {
+ it('renders the work item state', () => {
+ createComponent();
+
+ expect(findWorkItemState().exists()).toBe(true);
+ });
+ });
+
+ describe('assignees widget', () => {
+ it('renders assignees component when widget is returned from the API', () => {
+ createComponent();
+
+ expect(findWorkItemAssignees().exists()).toBe(true);
+ });
+
+ it('does not render assignees component when widget is not returned from the API', () => {
+ createComponent({
+ workItem: workItemResponseFactory({ assigneesWidgetPresent: false }).data.workItem,
+ });
+
+ expect(findWorkItemAssignees().exists()).toBe(false);
+ });
+ });
+
+ describe('labels widget', () => {
+ it.each`
+ description | labelsWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ labelsWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ labelsWidgetPresent });
+ createComponent({ workItem: response.data.workItem });
+
+ expect(findWorkItemLabels().exists()).toBe(exists);
+ });
+ });
+
+ describe('dates widget', () => {
+ describe.each`
+ description | datesWidgetPresent | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ datesWidgetPresent, exists }) => {
+ it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, () => {
+ const response = workItemResponseFactory({ datesWidgetPresent });
+ createComponent({ workItem: response.data.workItem });
+
+ expect(findWorkItemDueDate().exists()).toBe(exists);
+ });
+ });
+ });
+
+ describe('milestone widget', () => {
+ it.each`
+ description | milestoneWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ milestoneWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ milestoneWidgetPresent });
+ createComponent({ workItem: response.data.workItem });
+
+ expect(findWorkItemMilestone().exists()).toBe(exists);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
index 82be6d990e4..f8c5f8edc4c 100644
--- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
@@ -9,36 +9,67 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import AwardList from '~/vue_shared/components/awards_list.vue';
import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vue';
import updateAwardEmojiMutation from '~/work_items/graphql/update_award_emoji.mutation.graphql';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
+import workItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
+import {
+ EMOJI_THUMBSUP,
+ EMOJI_THUMBSDOWN,
+ DEFAULT_PAGE_SIZE_EMOJIS,
+ I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR,
+} from '~/work_items/constants';
import {
workItemByIidResponseFactory,
mockAwardsWidget,
mockAwardEmojiThumbsUp,
getAwardEmojiResponse,
+ mockMoreThanDefaultAwardEmojisWidget,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/work_items/constants', () => ({
+ ...jest.requireActual('~/work_items/constants'),
+ DEFAULT_PAGE_SIZE_EMOJIS: 5,
+}));
+
Vue.use(VueApollo);
describe('WorkItemAwardEmoji component', () => {
let wrapper;
let mockApolloProvider;
- const errorMessage = 'Failed to update the award';
+ const mutationErrorMessage = 'Failed to update the award';
+
const workItemQueryResponse = workItemByIidResponseFactory();
- const workItemQueryAddAwardEmojiResponse = workItemByIidResponseFactory({
- awardEmoji: { ...mockAwardsWidget, nodes: [mockAwardEmojiThumbsUp] },
- });
- const workItemQueryRemoveAwardEmojiResponse = workItemByIidResponseFactory({
- awardEmoji: { ...mockAwardsWidget, nodes: [] },
- });
+ const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
+
+ const awardEmojiQuerySuccessHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const awardEmojiQueryEmptyHandler = jest.fn().mockResolvedValue(
+ workItemByIidResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [],
+ },
+ }),
+ );
+ const awardEmojiQueryThumbsUpHandler = jest.fn().mockResolvedValue(
+ workItemByIidResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp],
+ },
+ }),
+ );
+ const awardEmojiQueryFailureHandler = jest
+ .fn()
+ .mockRejectedValue(new Error(I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR));
+
const awardEmojiAddSuccessHandler = jest.fn().mockResolvedValue(getAwardEmojiResponse(true));
const awardEmojiRemoveSuccessHandler = jest.fn().mockResolvedValue(getAwardEmojiResponse(false));
- const awardEmojiUpdateFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
- const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
- const mockAwardEmojiDifferentUserThumbsUp = {
+ const awardEmojiUpdateFailureHandler = jest
+ .fn()
+ .mockRejectedValue(new Error(mutationErrorMessage));
+
+ const mockAwardEmojiDifferentUser = {
name: 'thumbsup',
__typename: 'AwardEmoji',
user: {
@@ -49,35 +80,37 @@ describe('WorkItemAwardEmoji component', () => {
};
const createComponent = ({
- awardMutationHandler = awardEmojiAddSuccessHandler,
- workItem = mockWorkItem,
+ awardEmojiQueryHandler = awardEmojiQuerySuccessHandler,
+ awardEmojiMutationHandler = awardEmojiAddSuccessHandler,
workItemIid = '1',
- awardEmoji = { ...mockAwardsWidget, nodes: [] },
} = {}) => {
- mockApolloProvider = createMockApollo([[updateAwardEmojiMutation, awardMutationHandler]]);
-
- mockApolloProvider.clients.defaultClient.writeQuery({
- query: workItemByIidQuery,
- variables: { fullPath: workItem.project.fullPath, iid: workItemIid },
- data: {
- ...workItemQueryResponse.data,
- workspace: {
- __typename: 'Project',
- id: 'gid://gitlab/Project/1',
- workItems: {
- nodes: [workItem],
+ mockApolloProvider = createMockApollo(
+ [
+ [workItemAwardEmojiQuery, awardEmojiQueryHandler],
+ [updateAwardEmojiMutation, awardEmojiMutationHandler],
+ ],
+ {},
+ {
+ typePolicies: {
+ WorkItemWidgetAwardEmoji: {
+ fields: {
+ // If we add any key args, the awardEmoji field becomes awardEmoji({"first":10}) and
+ // kills any possibility to handle it on the widget level without hardcoding a string.
+ awardEmoji: {
+ keyArgs: false,
+ },
+ },
},
},
},
- });
+ );
wrapper = shallowMount(WorkItemAwardEmoji, {
isLoggedIn: isLoggedIn(),
apolloProvider: mockApolloProvider,
propsData: {
- workItemId: workItem.id,
- workItemFullpath: workItem.project.fullPath,
- awardEmoji,
+ workItemId: 'gid://gitlab/WorkItem/1',
+ workItemFullpath: 'test-project-path',
workItemIid,
},
});
@@ -85,17 +118,23 @@ describe('WorkItemAwardEmoji component', () => {
const findAwardsList = () => wrapper.findComponent(AwardList);
- beforeEach(() => {
+ beforeEach(async () => {
isLoggedIn.mockReturnValue(true);
window.gon = {
current_user_id: 5,
current_user_fullname: 'Dave Smith',
};
- createComponent();
+ await createComponent();
});
- it('renders the award-list component with default props', () => {
+ it('renders the award-list component with default props', async () => {
+ createComponent({
+ awardEmojiQueryHandler: awardEmojiQueryEmptyHandler,
+ });
+
+ await waitForPromises();
+
expect(findAwardsList().exists()).toBe(true);
expect(findAwardsList().props()).toEqual({
boundary: '',
@@ -108,8 +147,6 @@ describe('WorkItemAwardEmoji component', () => {
});
it('renders awards-list component with awards present', () => {
- createComponent({ awardEmoji: mockAwardsWidget });
-
expect(findAwardsList().props('awards')).toEqual([
{
name: EMOJI_THUMBSUP,
@@ -128,13 +165,32 @@ describe('WorkItemAwardEmoji component', () => {
]);
});
- it('renders awards list given by multiple users', () => {
+ it('emits error when there is an error while fetching award emojis', async () => {
createComponent({
+ awardEmojiQueryHandler: awardEmojiQueryFailureHandler,
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR]]);
+ });
+
+ it('renders awards list given by multiple users', async () => {
+ const mockWorkItemAwardEmojiDifferentUser = workItemByIidResponseFactory({
awardEmoji: {
...mockAwardsWidget,
- nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUserThumbsUp],
+ nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUser],
},
});
+ const awardEmojiWithDifferentUsersQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemAwardEmojiDifferentUser);
+
+ createComponent({
+ awardEmojiQueryHandler: awardEmojiWithDifferentUsersQueryHandler,
+ });
+
+ await waitForPromises();
expect(findAwardsList().props('awards')).toEqual([
{
@@ -155,21 +211,19 @@ describe('WorkItemAwardEmoji component', () => {
});
it.each`
- expectedAssertion | awardEmojiMutationHandler | mockAwardEmojiNodes | workItem
- ${'added'} | ${awardEmojiAddSuccessHandler} | ${[]} | ${workItemQueryRemoveAwardEmojiResponse.data.workspace.workItems.nodes[0]}
- ${'removed'} | ${awardEmojiRemoveSuccessHandler} | ${[mockAwardEmojiThumbsUp]} | ${workItemQueryAddAwardEmojiResponse.data.workspace.workItems.nodes[0]}
+ expectedAssertion | awardEmojiMutationHandler | awardEmojiQueryHandler
+ ${'added'} | ${awardEmojiAddSuccessHandler} | ${awardEmojiQueryEmptyHandler}
+ ${'removed'} | ${awardEmojiRemoveSuccessHandler} | ${awardEmojiQueryThumbsUpHandler}
`(
'calls mutation when an award emoji is $expectedAssertion',
- ({ awardEmojiMutationHandler, mockAwardEmojiNodes, workItem }) => {
+ async ({ awardEmojiMutationHandler, awardEmojiQueryHandler }) => {
createComponent({
- awardMutationHandler: awardEmojiMutationHandler,
- awardEmoji: {
- ...mockAwardsWidget,
- nodes: mockAwardEmojiNodes,
- },
- workItem,
+ awardEmojiMutationHandler,
+ awardEmojiQueryHandler,
});
+ await waitForPromises();
+
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
expect(awardEmojiMutationHandler).toHaveBeenCalledWith({
@@ -183,21 +237,24 @@ describe('WorkItemAwardEmoji component', () => {
it('emits error when the update mutation fails', async () => {
createComponent({
- awardMutationHandler: awardEmojiUpdateFailureHandler,
+ awardEmojiMutationHandler: awardEmojiUpdateFailureHandler,
+ awardEmojiQueryHandler: awardEmojiQueryEmptyHandler,
});
+ await waitForPromises();
+
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
+ expect(wrapper.emitted('error')).toEqual([[mutationErrorMessage]]);
});
describe('when user is not logged in', () => {
- beforeEach(() => {
+ beforeEach(async () => {
isLoggedIn.mockReturnValue(false);
- createComponent();
+ await createComponent();
});
it('renders the component with required props and canAwardEmoji false', () => {
@@ -213,15 +270,13 @@ describe('WorkItemAwardEmoji component', () => {
};
});
- it('calls mutation succesfully and adds the award emoji with proper user details', () => {
+ it('calls mutation succesfully and adds the award emoji with proper user details', async () => {
createComponent({
- awardMutationHandler: awardEmojiAddSuccessHandler,
- awardEmoji: {
- ...mockAwardsWidget,
- nodes: [mockAwardEmojiThumbsUp],
- },
+ awardEmojiMutationHandler: awardEmojiAddSuccessHandler,
});
+ await waitForPromises();
+
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
expect(awardEmojiAddSuccessHandler).toHaveBeenCalledWith({
@@ -232,4 +287,62 @@ describe('WorkItemAwardEmoji component', () => {
});
});
});
+
+ describe('pagination', () => {
+ describe('when there is no next page', () => {
+ const awardEmojiQuerySingleItemHandler = jest.fn().mockResolvedValue(
+ workItemByIidResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp],
+ },
+ }),
+ );
+
+ it('fetch more award emojis should not be called', async () => {
+ createComponent({ awardEmojiQueryHandler: awardEmojiQuerySingleItemHandler });
+ await waitForPromises();
+
+ expect(awardEmojiQuerySingleItemHandler).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_EMOJIS,
+ after: undefined,
+ });
+ expect(awardEmojiQuerySingleItemHandler).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when there is next page', () => {
+ const awardEmojisQueryMoreThanDefaultHandler = jest.fn().mockResolvedValueOnce(
+ workItemByIidResponseFactory({
+ awardEmoji: mockMoreThanDefaultAwardEmojisWidget,
+ }),
+ );
+
+ it('fetch more award emojis should be called', async () => {
+ createComponent({
+ awardEmojiQueryHandler: awardEmojisQueryMoreThanDefaultHandler,
+ });
+ await waitForPromises();
+
+ expect(awardEmojisQueryMoreThanDefaultHandler).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_EMOJIS,
+ after: 'endCursor',
+ });
+
+ await nextTick();
+
+ expect(awardEmojisQueryMoreThanDefaultHandler).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_EMOJIS,
+ after: mockMoreThanDefaultAwardEmojisWidget.pageInfo.endCursor,
+ });
+ expect(awardEmojisQueryMoreThanDefaultHandler).toHaveBeenCalledTimes(2);
+ });
+ });
+ });
});
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 b910e9854f8..8b9963b2476 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -12,14 +12,12 @@ import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
workItemByIidResponseFactory,
- workItemDescriptionSubscriptionResponse,
workItemQueryResponse,
} from '../mock_data';
@@ -34,7 +32,6 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
- const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
let workItemResponseHandler;
const findForm = () => wrapper.findComponent(GlForm);
@@ -63,7 +60,6 @@ describe('WorkItemDescription', () => {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
- [workItemDescriptionSubscription, subscriptionHandler],
]),
propsData: {
workItemId: id,
@@ -83,14 +79,6 @@ describe('WorkItemDescription', () => {
}
};
- it('has a subscription', async () => {
- await createComponent();
-
- expect(subscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
- });
-
describe('editing description', () => {
it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
const {
@@ -103,7 +91,6 @@ describe('WorkItemDescription', () => {
expect(findMarkdownEditor().props()).toMatchObject({
supportsQuickActions: true,
renderMarkdownPath: markdownPreviewPath(fullPath, iid),
- quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
autocompleteDataSources: autocompleteDataSources(fullPath, iid),
});
});
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 d8ba8ea74f2..7ceae935d2d 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -5,10 +5,11 @@ import {
GlSkeletonLoader,
GlButton,
GlEmptyState,
+ GlIntersectionObserver,
} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -18,12 +19,8 @@ import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
-import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
-import WorkItemState from '~/work_items/components/work_item_state.vue';
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
-import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
-import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
-import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
@@ -31,20 +28,13 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
-import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
-import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
-import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
+import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
import {
mockParent,
- workItemDatesSubscriptionResponse,
workItemByIidResponseFactory,
- workItemTitleSubscriptionResponse,
- workItemAssigneesSubscriptionResponse,
- workItemMilestoneSubscriptionResponse,
objectiveType,
mockWorkItemCommentNote,
} from '../mock_data';
@@ -63,16 +53,11 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
- const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const milestoneSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemMilestoneSubscriptionResponse);
- const assigneesSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemAssigneesSubscriptionResponse);
const showModalHandler = jest.fn();
const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0];
+ const workItemUpdatedSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue({ data: { workItemUpdated: null } });
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -81,42 +66,39 @@ describe('WorkItemDetail component', () => {
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
- const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
- const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
- const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
- const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
- const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
- const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
+ const findWorkItemAttributesWrapper = () => wrapper.findComponent(WorkItemAttributesWrapper);
+ const findParent = () => wrapper.findByTestId('work-item-parent');
const findParentButton = () => findParent().findComponent(GlButton);
- const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
- const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]');
+ const findCloseButton = () => wrapper.findByTestId('work-item-close');
+ const findWorkItemType = () => wrapper.findByTestId('work-item-type');
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findStickyHeader = () => wrapper.findByTestId('work-item-sticky-header');
+ const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
+ const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
+ const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
const createComponent = ({
isModal = false,
updateInProgress = false,
workItemIid = '1',
handler = successHandler,
- subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
workItemsMvc2Enabled = false,
} = {}) => {
const handlers = [
[workItemByIidQuery, handler],
- [workItemTitleSubscription, subscriptionHandler],
- [workItemDatesSubscription, datesSubscriptionHandler],
- [workItemAssigneesSubscription, assigneesSubscriptionHandler],
- [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
confidentialityMock,
];
- wrapper = shallowMount(WorkItemDetail, {
+ wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
isLoggedIn: isLoggedIn(),
propsData: {
@@ -163,13 +145,18 @@ describe('WorkItemDetail component', () => {
});
describe('when there is no `workItemIid` prop', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent({ workItemIid: null });
+ await waitForPromises();
});
it('skips the work item query', () => {
expect(successHandler).not.toHaveBeenCalled();
});
+
+ it('skips the work item updated subscription', () => {
+ expect(workItemUpdatedSubscriptionHandler).not.toHaveBeenCalled();
+ });
});
describe('when loading', () => {
@@ -179,7 +166,6 @@ describe('WorkItemDetail component', () => {
it('renders skeleton loader', () => {
expect(findSkeleton().exists()).toBe(true);
- expect(findWorkItemState().exists()).toBe(false);
expect(findWorkItemTitle().exists()).toBe(false);
});
});
@@ -192,7 +178,6 @@ describe('WorkItemDetail component', () => {
it('does not render skeleton', () => {
expect(findSkeleton().exists()).toBe(false);
- expect(findWorkItemState().exists()).toBe(true);
expect(findWorkItemTitle().exists()).toBe(true);
});
@@ -203,6 +188,10 @@ describe('WorkItemDetail component', () => {
it('renders todos widget if logged in', () => {
expect(findWorkItemTodos().exists()).toBe(true);
});
+
+ it('calls the work item updated subscription', () => {
+ expect(workItemUpdatedSubscriptionHandler).toHaveBeenCalledWith({ id });
+ });
});
describe('close button', () => {
@@ -488,159 +477,6 @@ describe('WorkItemDetail component', () => {
expect(findAlert().text()).toBe(updateError);
});
- describe('subscriptions', () => {
- it('calls the title subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(titleSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
-
- describe('assignees subscription', () => {
- describe('when the assignees widget exists', () => {
- it('calls the assignees subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
- });
-
- describe('when the assignees widget does not exist', () => {
- it('does not call the assignees subscription', async () => {
- const response = workItemByIidResponseFactory({ assigneesWidgetPresent: false });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(assigneesSubscriptionHandler).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('dates subscription', () => {
- describe('when the due date widget exists', () => {
- it('calls the dates subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(datesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
- });
-
- describe('when the due date widget does not exist', () => {
- it('does not call the dates subscription', async () => {
- const response = workItemByIidResponseFactory({ datesWidgetPresent: false });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(datesSubscriptionHandler).not.toHaveBeenCalled();
- });
- });
- });
- });
-
- describe('assignees widget', () => {
- it('renders assignees component when widget is returned from the API', async () => {
- createComponent();
- await waitForPromises();
-
- expect(findWorkItemAssignees().exists()).toBe(true);
- });
-
- it('does not render assignees component when widget is not returned from the API', async () => {
- createComponent({
- handler: jest
- .fn()
- .mockResolvedValue(workItemByIidResponseFactory({ assigneesWidgetPresent: false })),
- });
- await waitForPromises();
-
- expect(findWorkItemAssignees().exists()).toBe(false);
- });
- });
-
- describe('labels widget', () => {
- it.each`
- description | labelsWidgetPresent | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ labelsWidgetPresent, exists }) => {
- const response = workItemByIidResponseFactory({ labelsWidgetPresent });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(findWorkItemLabels().exists()).toBe(exists);
- });
- });
-
- describe('dates widget', () => {
- describe.each`
- description | datesWidgetPresent | exists
- ${'when widget is returned from API'} | ${true} | ${true}
- ${'when widget is not returned from API'} | ${false} | ${false}
- `('$description', ({ datesWidgetPresent, exists }) => {
- it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
- const response = workItemByIidResponseFactory({ datesWidgetPresent });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(findWorkItemDueDate().exists()).toBe(exists);
- });
- });
-
- it('shows an error message when it emits an `error` event', async () => {
- createComponent();
- await waitForPromises();
- const updateError = 'Failed to update';
-
- findWorkItemDueDate().vm.$emit('error', updateError);
- await waitForPromises();
-
- expect(findAlert().text()).toBe(updateError);
- });
- });
-
- describe('milestone widget', () => {
- it.each`
- description | milestoneWidgetPresent | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ milestoneWidgetPresent, exists }) => {
- const response = workItemByIidResponseFactory({ milestoneWidgetPresent });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(findWorkItemMilestone().exists()).toBe(exists);
- });
-
- describe('milestone subscription', () => {
- describe('when the milestone widget exists', () => {
- it('calls the milestone subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
- });
-
- describe('when the assignees widget does not exist', () => {
- it('does not call the milestone subscription', async () => {
- const response = workItemByIidResponseFactory({ milestoneWidgetPresent: false });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(milestoneSubscriptionHandler).not.toHaveBeenCalled();
- });
- });
- });
- });
-
it('calls the work item query', async () => {
createComponent();
await waitForPromises();
@@ -796,4 +632,76 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemTodos().exists()).toBe(false);
});
});
+
+ describe('work item attributes wrapper', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders the work item attributes wrapper', () => {
+ expect(findWorkItemAttributesWrapper().exists()).toBe(true);
+ });
+
+ it('shows an error message when it emits an `error` event', async () => {
+ const updateError = 'Failed to update';
+
+ findWorkItemAttributesWrapper().vm.$emit('error', updateError);
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(updateError);
+ });
+ });
+
+ describe('work item two column view', () => {
+ describe('when `workItemsMvc2Enabled` is false', () => {
+ beforeEach(async () => {
+ createComponent({ workItemsMvc2Enabled: false });
+ await waitForPromises();
+ });
+
+ it('does not have the `work-item-overview` class', () => {
+ expect(findWorkItemTwoColumnViewContainer().classes()).not.toContain('work-item-overview');
+ });
+
+ it('does not have sticky header', () => {
+ expect(findIntersectionObserver().exists()).toBe(false);
+ expect(findStickyHeader().exists()).toBe(false);
+ });
+
+ it('does not have right sidebar', () => {
+ expect(findRightSidebar().exists()).toBe(false);
+ });
+ });
+
+ describe('when `workItemsMvc2Enabled` is true', () => {
+ beforeEach(async () => {
+ createComponent({ workItemsMvc2Enabled: true });
+ await waitForPromises();
+ });
+
+ it('has the `work-item-overview` class', () => {
+ expect(findWorkItemTwoColumnViewContainer().classes()).toContain('work-item-overview');
+ });
+
+ it('does not show sticky header by default', () => {
+ expect(findStickyHeader().exists()).toBe(false);
+ });
+
+ it('has the sticky header when the page is scrolled', async () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+
+ global.pageYOffset = 100;
+ triggerPageScroll();
+
+ await nextTick();
+
+ expect(findStickyHeader().exists()).toBe(true);
+ });
+
+ it('has the right sidebar', () => {
+ expect(findRightSidebar().exists()).toBe(true);
+ });
+ });
+ });
});
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 6894aa236e3..4a20e654060 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -6,7 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
-import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
@@ -16,7 +15,6 @@ import {
mockLabels,
workItemByIidResponseFactory,
updateWorkItemMutationResponse,
- workItemLabelsSubscriptionResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -38,7 +36,6 @@ describe('WorkItemLabels component', () => {
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
- const subscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
@@ -53,7 +50,6 @@ describe('WorkItemLabels component', () => {
[workItemByIidQuery, workItemQueryHandler],
[labelSearchQuery, searchQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
- [workItemLabelsSubscription, subscriptionHandler],
]),
provide: {
fullPath: 'test-project-path',
@@ -246,16 +242,6 @@ describe('WorkItemLabels component', () => {
expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
});
-
- it('has a subscription', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(subscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemId,
- });
- });
});
it('calls the work item query', async () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index f3aa347f389..e90775a5240 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -1,12 +1,10 @@
import { nextTick } from 'vue';
-
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
-
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
@@ -42,9 +40,8 @@ describe('WorkItemTree', () => {
children,
canUpdate,
},
+ stubs: { WidgetWrapper },
});
-
- wrapper.vm.$refs.wrapper.show = jest.fn();
};
it('displays Add button', () => {
diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js
index 83b61a04298..454bd97bbee 100644
--- a/spec/frontend/work_items/components/work_item_todos_spec.js
+++ b/spec/frontend/work_items/components/work_item_todos_spec.js
@@ -1,14 +1,24 @@
import { GlButton, GlIcon } from '@gitlab/ui';
+
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
-import { ADD, TODO_DONE_ICON, TODO_ADD_ICON } from '~/work_items/constants';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ TODO_DONE_ICON,
+ TODO_ADD_ICON,
+ TODO_PENDING_STATE,
+ TODO_DONE_STATE,
+} from '~/work_items/constants';
import { updateGlobalTodoCount } from '~/sidebar/utils';
-import { workItemResponseFactory, updateWorkItemMutationResponseFactory } from '../mock_data';
+import createWorkItemTodosMutation from '~/work_items/graphql/create_work_item_todos.mutation.graphql';
+import markDoneWorkItemTodosMutation from '~/work_items/graphql/mark_done_work_item_todos.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+
+import { workItemResponseFactory, getTodosMutationResponse } from '../mock_data';
jest.mock('~/sidebar/utils');
@@ -22,27 +32,58 @@ describe('WorkItemTodo component', () => {
const errorMessage = 'Failed to add item';
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true });
- const successHandler = jest
+ const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+ const mockWorkItemIid = workItemQueryResponse.data.workItem.iid;
+ const mockWorkItemFullpath = workItemQueryResponse.data.workItem.project.fullPath;
+
+ const createTodoSuccessHandler = jest
.fn()
- .mockResolvedValue(updateWorkItemMutationResponseFactory({ canUpdate: true }));
+ .mockResolvedValue(getTodosMutationResponse(TODO_PENDING_STATE));
+ const markDoneTodoSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(getTodosMutationResponse(TODO_DONE_STATE));
const failureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
- const inputVariables = {
- id: 'gid://gitlab/WorkItem/1',
- currentUserTodosWidget: {
- action: ADD,
- },
+ const inputVariablesCreateTodos = {
+ targetId: 'gid://gitlab/WorkItem/1',
+ };
+
+ const inputVariablesMarkDoneTodos = {
+ id: 'gid://gitlab/Todo/1',
+ };
+
+ const mockCurrentUserTodos = {
+ id: 'gid://gitlab/Todo/1',
};
const createComponent = ({
- currentUserTodosMock = [updateWorkItemMutation, successHandler],
+ mutation = createWorkItemTodosMutation,
+ currentUserTodosHandler = createTodoSuccessHandler,
currentUserTodos = [],
} = {}) => {
- const handlers = [currentUserTodosMock];
+ const mockApolloProvider = createMockApollo([[mutation, currentUserTodosHandler]]);
+
+ mockApolloProvider.clients.defaultClient.cache.writeQuery({
+ query: workItemByIidQuery,
+ variables: { fullPath: mockWorkItemFullpath, iid: mockWorkItemIid },
+ data: {
+ ...workItemQueryResponse.data,
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+ });
+
wrapper = shallowMountExtended(WorkItemTodos, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: mockApolloProvider,
propsData: {
- workItem: workItemQueryResponse.data.workItem,
+ workItemId: mockWorkItemId,
+ workItemIid: mockWorkItemIid,
+ workItemFullpath: mockWorkItemFullpath,
currentUserTodos,
},
});
@@ -58,35 +99,41 @@ describe('WorkItemTodo component', () => {
it('renders mark as done button when there is pending item', () => {
createComponent({
- currentUserTodos: [
- {
- node: {
- id: 'gid://gitlab/Todo/1',
- state: 'pending',
- },
- },
- ],
+ currentUserTodos: [mockCurrentUserTodos],
});
expect(findTodoIcon().props('name')).toEqual(TODO_DONE_ICON);
expect(findTodoIcon().classes('gl-fill-blue-500')).toBe(true);
});
- it('calls update mutation when to do button is clicked', async () => {
- createComponent();
+ it.each`
+ assertionName | mutation | currentUserTodosHandler | currentUserTodos | inputVariables
+ ${'create'} | ${createWorkItemTodosMutation} | ${createTodoSuccessHandler} | ${[]} | ${inputVariablesCreateTodos}
+ ${'mark done'} | ${markDoneWorkItemTodosMutation} | ${markDoneTodoSuccessHandler} | ${[mockCurrentUserTodos]} | ${inputVariablesMarkDoneTodos}
+ `(
+ 'calls $assertionName todos mutation when to do button is toggled',
+ async ({ mutation, currentUserTodosHandler, currentUserTodos, inputVariables }) => {
+ createComponent({
+ mutation,
+ currentUserTodosHandler,
+ currentUserTodos,
+ });
- findTodoWidget().vm.$emit('click');
+ findTodoWidget().vm.$emit('click');
- await waitForPromises();
+ await waitForPromises();
- expect(successHandler).toHaveBeenCalledWith({
- input: inputVariables,
- });
- expect(updateGlobalTodoCount).toHaveBeenCalled();
- });
+ expect(currentUserTodosHandler).toHaveBeenCalledWith({
+ input: inputVariables,
+ });
+ expect(updateGlobalTodoCount).toHaveBeenCalled();
+ },
+ );
it('emits error when the update mutation fails', async () => {
- createComponent({ currentUserTodosMock: [updateWorkItemMutation, failureHandler] });
+ createComponent({
+ currentUserTodosHandler: failureHandler,
+ });
findTodoWidget().vm.$emit('click');
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index a873462ea63..f88e69a7ffe 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -68,6 +68,38 @@ export const mockAwardEmojiThumbsDown = {
export const mockAwardsWidget = {
nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiThumbsDown],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ __typename: 'AwardEmojiConnection',
+};
+
+export const mockMoreThanDefaultAwardEmojisWidget = {
+ nodes: [
+ mockAwardEmojiThumbsUp,
+ mockAwardEmojiThumbsDown,
+ { ...mockAwardEmojiThumbsUp, name: 'one' },
+ { ...mockAwardEmojiThumbsUp, name: 'two' },
+ { ...mockAwardEmojiThumbsUp, name: 'three' },
+ { ...mockAwardEmojiThumbsUp, name: 'four' },
+ { ...mockAwardEmojiThumbsUp, name: 'five' },
+ { ...mockAwardEmojiThumbsUp, name: 'six' },
+ { ...mockAwardEmojiThumbsUp, name: 'seven' },
+ { ...mockAwardEmojiThumbsUp, name: 'eight' },
+ { ...mockAwardEmojiThumbsUp, name: 'nine' },
+ { ...mockAwardEmojiThumbsUp, name: 'ten' },
+ ],
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: 'endCursor',
+ __typename: 'PageInfo',
+ },
__typename: 'AwardEmojiConnection',
};
@@ -629,14 +661,10 @@ export const workItemResponseFactory = ({
? {
type: 'CURRENT_USER_TODOS',
currentUserTodos: {
- edges: [
+ nodes: [
{
- node: {
- id: 'gid://gitlab/Todo/1',
- state: 'pending',
- __typename: 'Todo',
- },
- __typename: 'TodoEdge',
+ id: 'gid://gitlab/Todo/1',
+ __typename: 'Todo',
},
],
__typename: 'TodoConnection',
@@ -803,154 +831,6 @@ export const deleteWorkItemMutationErrorResponse = {
},
};
-export const workItemDatesSubscriptionResponse = {
- data: {
- issuableDatesUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetStartAndDueDate',
- dueDate: '2022-12-31',
- startDate: '2022-01-01',
- },
- ],
- },
- },
-};
-
-export const workItemTitleSubscriptionResponse = {
- data: {
- issuableTitleUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- title: 'new title',
- },
- },
-};
-
-export const workItemDescriptionSubscriptionResponse = {
- data: {
- issuableDescriptionUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetDescription',
- type: 'DESCRIPTION',
- description: 'New description',
- descriptionHtml: '<p>New description</p>',
- lastEditedAt: '2022-09-21T06:18:42Z',
- lastEditedBy: {
- id: 'gid://gitlab/User/2',
- name: 'Someone else',
- webPath: '/not-you',
- },
- },
- ],
- },
- },
-};
-
-export const workItemWeightSubscriptionResponse = {
- data: {
- issuableWeightUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetWeight',
- weight: 1,
- },
- ],
- },
- },
-};
-
-export const workItemAssigneesSubscriptionResponse = {
- data: {
- issuableAssigneesUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemAssigneesWeight',
- assignees: {
- nodes: [mockAssignees[0]],
- },
- },
- ],
- },
- },
-};
-
-export const workItemLabelsSubscriptionResponse = {
- data: {
- issuableLabelsUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetLabels',
- type: 'LABELS',
- allowsScopedLabels: false,
- labels: {
- nodes: mockLabels,
- },
- },
- ],
- },
- },
-};
-
-export const workItemIterationSubscriptionResponse = {
- data: {
- issuableIterationUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetIteration',
- iteration: {
- description: 'Iteration description',
- dueDate: '2022-07-29',
- id: 'gid://gitlab/Iteration/1125',
- iid: '95',
- startDate: '2022-06-22',
- title: 'Iteration subcription title',
- },
- },
- ],
- },
- },
-};
-
-export const workItemHealthStatusSubscriptionResponse = {
- data: {
- issuableHealthStatusUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetHealthStatus',
- healthStatus: 'needsAttention',
- },
- ],
- },
- },
-};
-
-export const workItemMilestoneSubscriptionResponse = {
- data: {
- issuableMilestoneUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetMilestone',
- type: 'MILESTONE',
- milestone: {
- id: 'gid://gitlab/Milestone/1125',
- expired: false,
- title: 'Milestone title',
- },
- },
- ],
- },
- },
-};
-
export const workItemHierarchyEmptyResponse = {
data: {
workspace: {
@@ -2130,6 +2010,9 @@ export const mockWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2241,6 +2124,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2294,6 +2180,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2348,6 +2237,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2460,6 +2352,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2513,6 +2408,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2564,6 +2462,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2631,6 +2532,9 @@ export const createWorkItemNoteResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2682,6 +2586,9 @@ export const mockWorkItemCommentNote = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
};
export const mockWorkItemCommentNoteByContributor = {
@@ -2781,6 +2688,9 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
__typename: 'Note',
},
{
@@ -2821,6 +2731,9 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2869,6 +2782,9 @@ export const mockWorkItemNotesResponseWithComments = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2945,6 +2861,9 @@ export const workItemNotesCreateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2972,6 +2891,9 @@ export const workItemNotesCreateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
},
@@ -3017,6 +2939,9 @@ export const workItemNotesUpdateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
},
@@ -3176,6 +3101,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3239,6 +3167,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3302,6 +3233,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3350,3 +3284,17 @@ export const getAwardEmojiResponse = (toggledOn) => {
},
};
};
+
+export const getTodosMutationResponse = (state) => {
+ return {
+ data: {
+ todoMutation: {
+ todo: {
+ id: 'gid://gitlab/Todo/1',
+ state,
+ },
+ errors: [],
+ },
+ },
+ };
+};
diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js
new file mode 100644
index 00000000000..8ae32ce5f40
--- /dev/null
+++ b/spec/frontend/work_items/notes/award_utils_spec.js
@@ -0,0 +1,109 @@
+import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import mockApollo from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import {
+ mockWorkItemNotesResponseWithComments,
+ mockAwardEmojiThumbsUp,
+ mockAwardEmojiThumbsDown,
+} from '../mock_data';
+
+function getWorkItem(data) {
+ return data.workspace.workItems.nodes[0];
+}
+function getFirstNote(workItem) {
+ return workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes.nodes[0];
+}
+
+describe('Work item note award utils', () => {
+ const workItem = getWorkItem(mockWorkItemNotesResponseWithComments.data);
+ const firstNote = getFirstNote(workItem);
+ const fullPath = 'test-project-path';
+ const workItemIid = workItem.iid;
+ const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsDown.user.id);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: currentUserId };
+ });
+
+ describe('getMutation', () => {
+ it('returns remove mutation when user has already awarded award', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsDown;
+
+ expect(getMutation({ note, name })).toEqual({
+ mutation: removeAwardEmojiMutation,
+ mutationName: 'awardEmojiRemove',
+ errorMessage: __('Failed to remove emoji. Please try again'),
+ });
+ });
+
+ it('returns remove mutation when user has not already awarded award', () => {
+ const note = {};
+ const { name } = mockAwardEmojiThumbsUp;
+
+ expect(getMutation({ note, name })).toEqual({
+ mutation: addAwardEmojiMutation,
+ mutationName: 'awardEmojiAdd',
+ errorMessage: __('Failed to add emoji. Please try again'),
+ });
+ });
+ });
+
+ describe('optimisticAwardUpdate', () => {
+ let apolloProvider;
+ beforeEach(() => {
+ apolloProvider = mockApollo();
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ ...mockWorkItemNotesResponseWithComments,
+ });
+ });
+
+ it('adds new emoji to cache', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsUp;
+
+ const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid });
+
+ updateFn(apolloProvider.clients.defaultClient.cache);
+
+ const updatedResult = apolloProvider.clients.defaultClient.readQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ });
+
+ const updatedWorkItem = getWorkItem(updatedResult);
+ const updatedNote = getFirstNote(updatedWorkItem);
+
+ expect(updatedNote.awardEmoji.nodes).toEqual([
+ mockAwardEmojiThumbsDown,
+ mockAwardEmojiThumbsUp,
+ ]);
+ });
+
+ it('removes existing emoji from cache', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsDown;
+
+ const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid });
+
+ updateFn(apolloProvider.clients.defaultClient.cache);
+
+ const updatedResult = apolloProvider.clients.defaultClient.readQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ });
+
+ const updatedWorkItem = getWorkItem(updatedResult);
+ const updatedNote = getFirstNote(updatedWorkItem);
+
+ expect(updatedNote.awardEmoji.nodes).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index b5d54a7c319..79ba31e7012 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -2,28 +2,14 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import {
- currentUserResponse,
- workItemAssigneesSubscriptionResponse,
- workItemDatesSubscriptionResponse,
- workItemByIidResponseFactory,
- workItemTitleSubscriptionResponse,
- workItemLabelsSubscriptionResponse,
- workItemMilestoneSubscriptionResponse,
- workItemDescriptionSubscriptionResponse,
-} from 'jest/work_items/mock_data';
+import { currentUserResponse, workItemByIidResponseFactory } from 'jest/work_items/mock_data';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import App from '~/work_items/components/app.vue';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
-import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
-import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
-import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
-import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
-import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
+import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -34,18 +20,9 @@ describe('Work items router', () => {
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
- const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
- const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const assigneesSubscriptionHandler = jest
+ const workItemUpdatedSubscriptionHandler = jest
.fn()
- .mockResolvedValue(workItemAssigneesSubscriptionResponse);
- const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
- const milestoneSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemMilestoneSubscriptionResponse);
- const descriptionSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemDescriptionSubscriptionResponse);
+ .mockResolvedValue({ data: { workItemUpdated: null } });
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
@@ -56,12 +33,7 @@ describe('Work items router', () => {
const handlers = [
[workItemByIidQuery, workItemQueryHandler],
[currentUserQuery, currentUserQueryHandler],
- [workItemDatesSubscription, datesSubscriptionHandler],
- [workItemTitleSubscription, titleSubscriptionHandler],
- [workItemAssigneesSubscription, assigneesSubscriptionHandler],
- [workItemLabelsSubscription, labelsSubscriptionHandler],
- [workItemMilestoneSubscription, milestoneSubscriptionHandler],
- [workItemDescriptionSubscription, descriptionSubscriptionHandler],
+ [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
];
wrapper = mount(App, {
@@ -81,6 +53,7 @@ describe('Work items router', () => {
WorkItemIteration: true,
WorkItemHealthStatus: true,
WorkItemNotes: true,
+ WorkItemAwardEmoji: true,
},
});
};
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index b8af5f10a5a..aa24b80cf08 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -1,9 +1,4 @@
-import {
- autocompleteDataSources,
- markdownPreviewPath,
- getWorkItemTodoOptimisticResponse,
-} from '~/work_items/utils';
-import { workItemResponseFactory } from './mock_data';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
describe('autocompleteDataSources', () => {
beforeEach(() => {
@@ -30,17 +25,3 @@ describe('markdownPreviewPath', () => {
);
});
});
-
-describe('getWorkItemTodoOptimisticResponse', () => {
- it.each`
- scenario | pendingTodo | result
- ${'empty'} | ${false} | ${0}
- ${'present'} | ${true} | ${1}
- `('returns correct response when pending item list is $scenario', ({ pendingTodo, result }) => {
- const workItem = workItemResponseFactory({ canUpdate: true });
- expect(
- getWorkItemTodoOptimisticResponse({ workItem, pendingTodo }).workItemUpdate.workItem
- .widgets[0].currentUserTodos.edges.length,
- ).toBe(result);
- });
-});
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 6bafe609995..8419c7aae63 100644
--- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js
+++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
@@ -22,6 +22,15 @@ describe('content_editor', () => {
listeners: {
...listeners,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index b5c2d4da9ac..885bbc82ecc 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema do
- let_it_be(:connections) { GitlabSchema.connections.all_wrappers }
+ let_it_be(:connections) { described_class.connections.all_wrappers }
let_it_be(:tracers) { described_class.tracers }
let(:user) { build :user }
@@ -12,10 +12,6 @@ RSpec.describe GitlabSchema do
expect(tracers).to include(BatchLoader::GraphQL)
end
- it 'enables the generic instrumenter' do
- expect(tracers).to include(instance_of(::Gitlab::Graphql::GenericTracing))
- end
-
it 'has the base mutation' do
expect(described_class.mutation).to eq(::Types::MutationType)
end
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
index 864818351a1..3f58f2678d8 100644
--- a/spec/graphql/graphql_triggers_spec.rb
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
)
- GraphqlTriggers.issuable_assignees_updated(issuable)
+ described_class.issuable_assignees_updated(issuable)
end
end
@@ -32,7 +32,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
).and_call_original
- GraphqlTriggers.issuable_title_updated(issuable)
+ described_class.issuable_title_updated(issuable)
end
end
@@ -44,7 +44,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
).and_call_original
- GraphqlTriggers.issuable_description_updated(issuable)
+ described_class.issuable_description_updated(issuable)
end
end
@@ -62,7 +62,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
)
- GraphqlTriggers.issuable_labels_updated(issuable)
+ described_class.issuable_labels_updated(issuable)
end
end
@@ -74,7 +74,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
).and_call_original
- GraphqlTriggers.issuable_dates_updated(issuable)
+ described_class.issuable_dates_updated(issuable)
end
end
@@ -86,7 +86,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
).and_call_original
- GraphqlTriggers.issuable_milestone_updated(issuable)
+ described_class.issuable_milestone_updated(issuable)
end
end
@@ -100,7 +100,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
merge_request
).and_call_original
- GraphqlTriggers.merge_request_reviewers_updated(merge_request)
+ described_class.merge_request_reviewers_updated(merge_request)
end
end
@@ -114,7 +114,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
merge_request
).and_call_original
- GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ described_class.merge_request_merge_status_updated(merge_request)
end
end
@@ -128,7 +128,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
merge_request
).and_call_original
- GraphqlTriggers.merge_request_approval_state_updated(merge_request)
+ described_class.merge_request_approval_state_updated(merge_request)
end
end
@@ -140,7 +140,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
issuable
).and_call_original
- GraphqlTriggers.work_item_updated(issuable)
+ described_class.work_item_updated(issuable)
end
context 'when triggered with an Issue' do
@@ -154,7 +154,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do
work_item
).and_call_original
- GraphqlTriggers.work_item_updated(issue)
+ described_class.work_item_updated(issue)
end
end
end
diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
index 164bd9b1e39..c92aeb43f51 100644
--- a/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
+++ b/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
- let(:args) { { project_path: project.full_path, active: true, api_url: 'http://prometheus.com/' } }
+ let(:api_url) { 'http://prometheus.com/' }
+ let(:args) { { project_path: project.full_path, active: true, api_url: api_url } }
specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
@@ -29,6 +30,14 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
end
end
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it 'creates the integration' do
+ expect { resolve }.to change(::Alerting::ProjectAlertingSetting, :count).by(1)
+ end
+ end
+
context 'when UpdateService responds with success' do
it 'returns the integration with no errors' do
expect(resolve).to eq(
@@ -38,7 +47,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
end
it 'creates a corresponding token' do
- expect { resolve }.to change(::Alerting::ProjectAlertingSetting, :count).by(1)
+ expect { resolve }.to change(::Integrations::Prometheus, :count).by(1)
end
end
diff --git a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index 0485796fe56..54da3061323 100644
--- a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -20,10 +20,6 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddProject, feature_category: :cont
mutation.resolve(**mutation_args)
end
- before do
- stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
- end
-
context 'when user is not logged in' do
let(:current_user) { nil }
@@ -75,42 +71,6 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddProject, feature_category: :cont
end
end
- context 'when FF frozen_outbound_job_token_scopes is disabled' do
- before do
- stub_feature_flags(frozen_outbound_job_token_scopes: false)
- end
-
- it 'adds target project to the outbound job token scope by default' do
- expect do
- expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
- end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
- end
-
- context 'when mutation uses the direction argument' do
- let(:mutation_args) { super().merge!(direction: direction) }
-
- context 'when targeting the outbound allowlist' do
- let(:direction) { :outbound }
-
- it 'adds the target project' do
- expect do
- expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
- end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
- end
- end
-
- context 'when targeting the inbound allowlist' do
- let(:direction) { :inbound }
-
- it 'adds the target project' do
- expect do
- expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
- end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1)
- end
- end
- end
- end
-
context 'when the service returns an error' do
let(:service) { double(:service) }
diff --git a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
index 564bc95b352..a932002d614 100644
--- a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
+++ b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do
specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') }
- it { expect(described_class.arguments.keys).to match_array(%w[key value variableType]) }
+ it { expect(described_class.arguments.keys).to match_array(%w[id key value variableType destroy]) }
end
diff --git a/spec/graphql/mutations/issues/create_spec.rb b/spec/graphql/mutations/issues/create_spec.rb
index 24348097021..04b437b27b5 100644
--- a/spec/graphql/mutations/issues/create_spec.rb
+++ b/spec/graphql/mutations/issues/create_spec.rb
@@ -19,8 +19,7 @@ RSpec.describe Mutations::Issues::Create do
description: 'new description',
confidential: true,
due_date: Date.tomorrow,
- discussion_locked: true,
- issue_type: 'issue'
+ discussion_locked: true
}
end
@@ -29,7 +28,8 @@ RSpec.describe Mutations::Issues::Create do
project_path: project.full_path,
milestone_id: milestone.to_global_id,
labels: [project_label1.title, project_label2.title, new_label1, new_label2],
- assignee_ids: [assignee1.to_global_id, assignee2.to_global_id]
+ assignee_ids: [assignee1.to_global_id, assignee2.to_global_id],
+ issue_type: 'issue'
}.merge(expected_attributes)
end
diff --git a/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb
index 0f40565c5d3..80234aaaacf 100644
--- a/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb
+++ b/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do
+RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver, feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:guest) { create(:user) }
@@ -13,6 +13,7 @@ RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do
let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_proj_integration) { create(:alert_management_http_integration) }
+ let_it_be(:migrated_integration) { create(:alert_management_prometheus_integration, :legacy, project: project) }
let(:params) { {} }
diff --git a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb
index 11114d41522..ed2e7d35ee3 100644
--- a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb
+++ b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do
+RSpec.describe Resolvers::AlertManagement::IntegrationsResolver, feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
@@ -13,6 +13,7 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_proj_integration) { create(:alert_management_http_integration, project: project2) }
let_it_be(:other_proj_prometheus_integration) { create(:prometheus_integration, project: project2) }
+ let_it_be(:migrated_integration) { create(:alert_management_prometheus_integration, :legacy, project: project) }
let(:params) { {} }
diff --git a/spec/graphql/resolvers/board_lists_resolver_spec.rb b/spec/graphql/resolvers/board_lists_resolver_spec.rb
index 0f6e51ebbd0..1de59c5f507 100644
--- a/spec/graphql/resolvers/board_lists_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_lists_resolver_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Resolvers::BoardListsResolver do
end
it 'does not create the backlog list' do
+ board.lists.backlog.delete_all
lists = resolve_board_lists
expect(lists.count).to eq 1
@@ -35,7 +36,6 @@ RSpec.describe Resolvers::BoardListsResolver do
context 'when authorized' do
let!(:label_list) { create(:list, board: board, label: label) }
- let!(:backlog_list) { create(:backlog_list, board: board) }
it 'returns a list of board lists' do
lists = resolve_board_lists
diff --git a/spec/graphql/resolvers/ci/config_resolver_spec.rb b/spec/graphql/resolvers/ci/config_resolver_spec.rb
index 692bdf58784..16a2286cb7e 100644
--- a/spec/graphql/resolvers/ci/config_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/config_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::ConfigResolver do
+RSpec.describe Resolvers::Ci::ConfigResolver, feature_category: :continuous_integration do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/resolvers/ci/inherited_variables_resolver_spec.rb b/spec/graphql/resolvers/ci/inherited_variables_resolver_spec.rb
index 6837d4b0459..e1a76ce0f0f 100644
--- a/spec/graphql/resolvers/ci/inherited_variables_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/inherited_variables_resolver_spec.rb
@@ -11,15 +11,16 @@ RSpec.describe Resolvers::Ci::InheritedVariablesResolver, feature_category: :sec
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, group: subgroup) }
let_it_be(:project_without_group) { create(:project) }
+ let_it_be(:variable1) { create(:ci_group_variable, group: group, key: 'GROUP_VAR_A', created_at: 1.day.ago) }
+ let_it_be(:variable2) { create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B') }
let_it_be(:inherited_ci_variables) do
- [
- create(:ci_group_variable, group: group, key: 'GROUP_VAR_A'),
- create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B')
- ]
+ [variable1, variable2]
end
- subject(:resolve_variables) { resolve(described_class, obj: obj, ctx: { current_user: user }, args: {}) }
+ let(:args) { {} }
+
+ subject(:resolve_variables) { resolve(described_class, obj: obj, args: args, ctx: { current_user: user }) }
context 'when project does not have a group' do
let_it_be(:obj) { project_without_group }
@@ -36,5 +37,47 @@ RSpec.describe Resolvers::Ci::InheritedVariablesResolver, feature_category: :sec
expect(resolve_variables.items.to_a).to match_array(inherited_ci_variables)
end
end
+
+ describe 'sorting behaviour' do
+ let_it_be(:obj) { project }
+
+ context 'with sort by default (created_at descending)' do
+ it 'returns variables ordered by created_at in descending order' do
+ expect(resolve_variables.items.to_a).to eq([variable2, variable1])
+ end
+ end
+
+ context 'with sort by created_at descending' do
+ let(:args) { { sort: 'CREATED_DESC' } }
+
+ it 'returns variables ordered by created_at in descending order' do
+ expect(resolve_variables.items.to_a).to eq([variable2, variable1])
+ end
+ end
+
+ context 'with sort by created_at ascending' do
+ let(:args) { { sort: 'CREATED_ASC' } }
+
+ it 'returns variables ordered by created_at in ascending order' do
+ expect(resolve_variables.items.to_a).to eq([variable1, variable2])
+ end
+ end
+
+ context 'with sort by key descending' do
+ let(:args) { { sort: 'KEY_DESC' } }
+
+ it 'returns variables ordered by key in descending order' do
+ expect(resolve_variables.items.to_a).to eq([variable2, variable1])
+ end
+ end
+
+ context 'with sort by key ascending' do
+ let(:args) { { sort: 'KEY_ASC' } }
+
+ it 'returns variables ordered by key in ascending order' do
+ expect(resolve_variables.items.to_a).to eq([variable1, variable2])
+ end
+ end
+ end
end
end
diff --git a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
index 92f4d3dd8e8..d2bcc7cd597 100644
--- a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
+RSpec.describe Resolvers::Ci::JobTokenScopeResolver, feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb b/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb
index 07b4a5509b2..e04bed0f077 100644
--- a/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/project_pipeline_counts_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::ProjectPipelineCountsResolver do
+RSpec.describe Resolvers::Ci::ProjectPipelineCountsResolver, feature_category: :continuous_integration do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/graphql/resolvers/ci/runner_job_count_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_job_count_resolver_spec.rb
new file mode 100644
index 00000000000..6336ea883f7
--- /dev/null
+++ b/spec/graphql/resolvers/ci/runner_job_count_resolver_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::RunnerJobCountResolver, feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:irrelevant_pipeline) { create(:ci_pipeline, project: project) }
+
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
+
+ let_it_be(:build_one) { create(:ci_build, :success, name: 'Build One', runner: runner, pipeline: pipeline) }
+ let_it_be(:build_two) { create(:ci_build, :success, name: 'Build Two', runner: runner, pipeline: pipeline) }
+ let_it_be(:build_three) { create(:ci_build, :failed, name: 'Build Three', runner: runner, pipeline: pipeline) }
+ let_it_be(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline) }
+
+ describe '#resolve' do
+ subject(:job_count) { resolve_job_count(args) }
+
+ let(:args) { {} }
+
+ context 'with authorized user', :enable_admin_mode do
+ let(:current_user) { create(:user, :admin) }
+
+ context 'with statuses argument filtering on successful builds' do
+ let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+
+ it { is_expected.to eq 2 }
+ end
+
+ context 'with statuses argument filtering on failed builds' do
+ let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] } }
+
+ it { is_expected.to eq 1 }
+ end
+
+ context 'without statuses argument' do
+ it { is_expected.to eq 3 }
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ private
+
+ def resolve_job_count(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: runner, args: args, ctx: context)&.value
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
index 963a642fa4e..322bead0d3c 100644
--- a/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
@@ -9,17 +9,18 @@ RSpec.describe Resolvers::Ci::RunnerJobsResolver, feature_category: :runner_flee
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:irrelevant_pipeline) { create(:ci_pipeline, project: project) }
- let!(:build_one) { create(:ci_build, :success, name: 'Build One', runner: runner, pipeline: pipeline) }
- let!(:build_two) { create(:ci_build, :success, name: 'Build Two', runner: runner, pipeline: pipeline) }
- let!(:build_three) { create(:ci_build, :failed, name: 'Build Three', runner: runner, pipeline: pipeline) }
- let!(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline) }
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
- let(:args) { {} }
- let(:runner) { create(:ci_runner, :project, projects: [project]) }
-
- subject { resolve_jobs(args) }
+ let_it_be(:build_one) { create(:ci_build, :success, name: 'Build One', runner: runner, pipeline: pipeline) }
+ let_it_be(:build_two) { create(:ci_build, :success, name: 'Build Two', runner: runner, pipeline: pipeline) }
+ let_it_be(:build_three) { create(:ci_build, :failed, name: 'Build Three', runner: runner, pipeline: pipeline) }
+ let_it_be(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline) }
describe '#resolve' do
+ subject(:jobs) { resolve_jobs(args) }
+
+ let(:args) { {} }
+
context 'with authorized user', :enable_admin_mode do
let(:current_user) { create(:user, :admin) }
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index e4620b96cae..02fc7d28255 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
let(:obj) { nil }
let(:args) { {} }
- subject do
+ subject(:resolve_scope) do
resolve(described_class, obj: obj, ctx: { current_user: user }, args: args,
arg_style: :internal)
end
diff --git a/spec/graphql/resolvers/design_management/version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_resolver_spec.rb
index ab1d7d4d9c5..b72b02bf89b 100644
--- a/spec/graphql/resolvers/design_management/version_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/version_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::DesignManagement::VersionResolver do
+RSpec.describe Resolvers::DesignManagement::VersionResolver, feature_category: :shared do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/graphql/resolvers/echo_resolver_spec.rb b/spec/graphql/resolvers/echo_resolver_spec.rb
index 59a121ac7de..02ec7327f74 100644
--- a/spec/graphql/resolvers/echo_resolver_spec.rb
+++ b/spec/graphql/resolvers/echo_resolver_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Resolvers::EchoResolver do
describe '#resolve' do
it 'echoes text and username' do
- expect(resolve_echo(text)).to eq %Q("#{current_user.username}" says: #{text})
+ expect(resolve_echo(text)).to eq %("#{current_user.username}" says: #{text})
end
it 'echoes text and nil as username' do
diff --git a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
index 86a4154f23b..012c40e358f 100644
--- a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
+++ b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe Resolvers::IssueStatusCountsResolver do
context 'when both assignee_username and assignee_usernames are provided' do
it 'returns a mutually exclusive filter error' do
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername, assigneeWildcardId] arguments is allowed at the same time.') do
resolve_issue_status_counts(assignee_usernames: [current_user.username], assignee_username: current_user.username)
end
end
diff --git a/spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb b/spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb
deleted file mode 100644
index 354fd350aa7..00000000000
--- a/spec/graphql/resolvers/metrics/dashboard_resolver_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Resolvers::Metrics::DashboardResolver, feature_category: :metrics do
- include GraphqlHelpers
-
- let_it_be(:current_user) { create(:user) }
-
- describe '#resolve' do
- subject(:resolve_dashboard) { resolve(described_class, obj: parent_object, args: args, ctx: { current_user: current_user }) }
-
- let(:args) do
- {
- path: 'config/prometheus/common_metrics.yml'
- }
- end
-
- context 'for environment' do
- let(:project) { create(:project) }
- let(:parent_object) { create(:environment, project: project) }
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- project.add_developer(current_user)
- end
-
- it 'use ActiveModel class to find matching dashboard', :aggregate_failures do
- expected_arguments = { project: project, user: current_user, path: args[:path], options: { environment: parent_object } }
-
- expect(PerformanceMonitoring::PrometheusDashboard).to receive(:find_for).with(expected_arguments).and_return(PerformanceMonitoring::PrometheusDashboard.new)
- expect(resolve_dashboard).to be_instance_of PerformanceMonitoring::PrometheusDashboard
- end
-
- context 'without parent object' do
- let(:parent_object) { nil }
-
- it 'returns nil', :aggregate_failures do
- expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:find_for)
- expect(resolve_dashboard).to be_nil
- end
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it 'returns nil', :aggregate_failures do
- expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:find_for)
- expect(resolve_dashboard).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/graphql/resolvers/project_issues_resolver_spec.rb b/spec/graphql/resolvers/project_issues_resolver_spec.rb
index a510baab5a9..faafbc465e3 100644
--- a/spec/graphql/resolvers/project_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_issues_resolver_spec.rb
@@ -195,7 +195,7 @@ RSpec.describe Resolvers::ProjectIssuesResolver do
context 'when both assignee_username and assignee_usernames are provided' do
it 'generates a mutually exclusive filter error' do
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername, assigneeWildcardId] arguments is allowed at the same time.') do
resolve_issues(assignee_usernames: [assignee.username], assignee_username: assignee.username)
end
end
diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb
index 63a14daabba..22111626c5b 100644
--- a/spec/graphql/resolvers/users/participants_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb
@@ -137,7 +137,8 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
# 1 extra query per source (3 emojis + 2 notes) to fetch participables collection
# 2 extra queries to load work item widgets collection
- expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(7)
+ # 1 extra query to load the project creator to check if they are banned
+ expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(8)
end
it 'does not execute N+1 for system note metadata relation' do
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
index 4428fc0683a..92e8104fc4d 100644
--- a/spec/graphql/types/alert_management/alert_type_spec.rb
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'], feature_category: :in
assignees
notes
discussions
- metrics_dashboard_url
runbook
todos
details_url
diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb
index 81ab1b52552..69fb2bc43c0 100644
--- a/spec/graphql/types/ci/detailed_status_type_spec.rb
+++ b/spec/graphql/types/ci/detailed_status_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::Ci::DetailedStatusType do
include GraphqlHelpers
- let_it_be(:stage) { create(:ci_stage, status: :skipped) }
+ let_it_be(:stage) { create(:ci_stage, status: :manual) }
specify { expect(described_class.graphql_name).to eq('DetailedStatus') }
diff --git a/spec/graphql/types/ci/group_variable_type_spec.rb b/spec/graphql/types/ci/group_variable_type_spec.rb
index 106935642f2..ef6d8279523 100644
--- a/spec/graphql/types/ci/group_variable_type_spec.rb
+++ b/spec/graphql/types/ci/group_variable_type_spec.rb
@@ -5,5 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiGroupVariable'] do
specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
- specify { expect(described_class).to have_graphql_fields(:environment_scope, :masked, :protected).at_least }
+ specify do
+ expect(described_class).to have_graphql_fields(:environment_scope, :masked, :protected, :description).at_least
+ end
end
diff --git a/spec/graphql/types/ci/project_variable_type_spec.rb b/spec/graphql/types/ci/project_variable_type_spec.rb
index e6e045b2bca..cec0753fcba 100644
--- a/spec/graphql/types/ci/project_variable_type_spec.rb
+++ b/spec/graphql/types/ci/project_variable_type_spec.rb
@@ -5,5 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiProjectVariable'] do
specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
- specify { expect(described_class).to have_graphql_fields(:environment_scope, :masked, :protected).at_least }
+ specify do
+ expect(described_class).to have_graphql_fields(:environment_scope, :masked, :protected, :description).at_least
+ end
end
diff --git a/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb b/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
index a0d99f5f0c1..7fc600745df 100644
--- a/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
+++ b/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['VerificationStatus'] do
.to match_array(%w[
UNVERIFIED UNVERIFIED_KEY VERIFIED
SAME_USER_DIFFERENT_EMAIL OTHER_USER UNKNOWN_KEY
- MULTIPLE_SIGNATURES REVOKED_KEY
+ MULTIPLE_SIGNATURES REVOKED_KEY VERIFIED_SYSTEM
])
end
end
diff --git a/spec/graphql/types/detployment_tag_type_spec.rb b/spec/graphql/types/deployment_tag_type_spec.rb
index 9a7a8db0970..b6741c208fe 100644
--- a/spec/graphql/types/detployment_tag_type_spec.rb
+++ b/spec/graphql/types/deployment_tag_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['DeploymentTag'] do
+RSpec.describe GitlabSchema.types['DeploymentTag'], feature_category: :continuous_delivery do
specify { expect(described_class.graphql_name).to eq('DeploymentTag') }
it 'has the expected fields' do
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index 721c20efc81..1d1bc4b2cb4 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'includes the expected fields' do
expected_fields = %w[
- name id state metrics_dashboard latest_opened_most_severe_alert path external_url deployments
+ name id state latest_opened_most_severe_alert path external_url deployments
slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment deployFreezes
clusterAgent
]
diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb
index fa0b34113bc..8ce0bc2b70a 100644
--- a/spec/graphql/types/global_id_type_spec.rb
+++ b/spec/graphql/types/global_id_type_spec.rb
@@ -105,12 +105,12 @@ RSpec.describe Types::GlobalIDType do
around do |example|
# Unset all previously memoized GlobalIDTypes to allow us to define one
# that will use the constants stubbed in the `before` block.
- previous_id_types = Types::GlobalIDType.instance_variable_get(:@id_types)
- Types::GlobalIDType.instance_variable_set(:@id_types, {})
+ previous_id_types = described_class.instance_variable_get(:@id_types)
+ described_class.instance_variable_set(:@id_types, {})
example.run
ensure
- Types::GlobalIDType.instance_variable_set(:@id_types, previous_id_types)
+ described_class.instance_variable_set(:@id_types, previous_id_types)
end
before do
diff --git a/spec/graphql/types/ide_type_spec.rb b/spec/graphql/types/ide_type_spec.rb
new file mode 100644
index 00000000000..b0e43332fa8
--- /dev/null
+++ b/spec/graphql/types/ide_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Ide'], feature_category: :web_ide do
+ specify { expect(described_class.graphql_name).to eq('Ide') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ codeSuggestionsEnabled
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index a9fe85ac62f..7c4f2a06147 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -266,7 +266,6 @@ RSpec.describe GitlabSchema.types['Issue'] do
context 'for an incident' do
before do
issue.update!(
- issue_type: WorkItems::Type.base_types[:incident],
work_item_type: WorkItems::Type.default_by_type(:incident)
)
end
diff --git a/spec/graphql/types/metrics/dashboard_type_spec.rb b/spec/graphql/types/metrics/dashboard_type_spec.rb
deleted file mode 100644
index 114db87d5f1..00000000000
--- a/spec/graphql/types/metrics/dashboard_type_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe GitlabSchema.types['MetricsDashboard'] do
- specify { expect(described_class.graphql_name).to eq('MetricsDashboard') }
-
- it 'has the expected fields' do
- expected_fields = %w[
- path annotations schema_validation_warnings
- ]
-
- expect(described_class).to have_graphql_fields(*expected_fields)
- end
-
- describe 'annotations field' do
- subject { described_class.fields['annotations'] }
-
- it { is_expected.to have_graphql_type(Types::Metrics::Dashboards::AnnotationType.connection_type) }
- it { is_expected.to have_graphql_resolver(Resolvers::Metrics::Dashboards::AnnotationResolver) }
- end
-end
diff --git a/spec/graphql/types/project_statistics_type_spec.rb b/spec/graphql/types/project_statistics_type_spec.rb
index a958a5150aa..558ff41f6f4 100644
--- a/spec/graphql/types/project_statistics_type_spec.rb
+++ b/spec/graphql/types/project_statistics_type_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ProjectStatistics'] do
- it 'has all the required fields' do
- expect(described_class).to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
+ it 'has the expected fields' do
+ expect(described_class).to include_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
:build_artifacts_size, :packages_size, :commit_count,
:wiki_size, :snippets_size, :pipeline_artifacts_size,
:uploads_size, :container_registry_size)
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index bcfdb05ca90..262164a0821 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -909,6 +909,14 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(forks).to contain_exactly(a_hash_including('fullPath' => fork_developer.full_path),
a_hash_including('fullPath' => fork_group_developer.full_path))
end
+
+ context 'when current user is not set' do
+ let(:user) { nil }
+
+ it 'does not return any forks' do
+ expect(forks.count).to eq(0)
+ end
+ end
end
end
end
diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb
index 56f58825db0..00f4092baf4 100644
--- a/spec/graphql/types/root_storage_statistics_type_spec.rb
+++ b/spec/graphql/types/root_storage_statistics_type_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['RootStorageStatistics'] do
specify { expect(described_class.graphql_name).to eq('RootStorageStatistics') }
- it 'has all the required fields' do
- expect(described_class).to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
+ it 'has the expected fields' do
+ expect(described_class).to include_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, :registry_size_estimated)
+ :container_registry_size, :container_registry_size_is_estimated, :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 777972df88b..af0f8a86b6c 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -55,6 +55,8 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
organization
jobTitle
createdAt
+ pronouns
+ ide
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -77,13 +79,13 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
let(:username) { requested_user.username }
let(:query) do
- %(
+ <<~GQL
query {
user(username: "#{username}") {
name
}
}
- )
+ GQL
end
subject(:user_name) { GitlabSchema.execute(query, context: { current_user: current_user }).as_json.dig('data', 'user', 'name') }
@@ -254,4 +256,40 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
is_expected.to have_graphql_type(Types::Users::NamespaceCommitEmailType.connection_type)
end
end
+
+ describe 'ide field' do
+ subject { described_class.fields['ide'] }
+
+ it 'returns ide' do
+ is_expected.to have_graphql_type(Types::IdeType)
+ end
+
+ context 'code suggestions enabled' do
+ let(:current_user) { create(:user) }
+ let(:query) do
+ <<~GQL
+ query {
+ currentUser {
+ ide {
+ codeSuggestionsEnabled
+ }
+ }
+ }
+ GQL
+ end
+
+ subject(:code_suggestions_enabled) do
+ GitlabSchema.execute(query, context: { current_user: current_user })
+ .as_json
+ .dig('data', 'currentUser', 'ide', 'codeSuggestionsEnabled')
+ end
+
+ it 'returns code suggestions enabled' do
+ allow(current_user).to receive(:can?).with(:access_code_suggestions).and_return(true)
+
+ expect(current_user).to receive(:can?).with(:access_code_suggestions).and_return(true)
+ expect(code_suggestions_enabled).to be true
+ end
+ end
+ end
end
diff --git a/spec/helpers/admin/application_settings/settings_helper_spec.rb b/spec/helpers/admin/application_settings/settings_helper_spec.rb
index efffc224eb2..b008f52c0eb 100644
--- a/spec/helpers/admin/application_settings/settings_helper_spec.rb
+++ b/spec/helpers/admin/application_settings/settings_helper_spec.rb
@@ -33,6 +33,12 @@ RSpec.describe Admin::ApplicationSettings::SettingsHelper do
end
describe 'Code Suggestions for Self-Managed instances', feature_category: :code_suggestions do
+ describe '#code_suggestions_description' do
+ subject { helper.code_suggestions_description }
+
+ it { is_expected.to include 'https://docs.gitlab.com/ee/user/project/repository/code_suggestions.html' }
+ end
+
describe '#code_suggestions_token_explanation' do
subject { helper.code_suggestions_token_explanation }
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 01be083b506..6ef57f8e22c 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -200,6 +200,60 @@ RSpec.describe ApplicationHelper do
it { expect(helper.active_when(false)).to eq(nil) }
end
+ describe '#linkedin_url?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:user) { build_stubbed(:user) }
+
+ subject { helper.linkedin_url(user) }
+
+ before do
+ user.linkedin = linkedin_name
+ end
+
+ where(:linkedin_name, :linkedin_url) do
+ nil | 'https://www.linkedin.com/in/'
+ '' | 'https://www.linkedin.com/in/'
+ 'alice' | 'https://www.linkedin.com/in/alice'
+ 'http://www.linkedin.com/in/alice' | 'http://www.linkedin.com/in/alice'
+ 'http://linkedin.com/in/alice' | 'http://linkedin.com/in/alice'
+ 'https://www.linkedin.com/in/alice' | 'https://www.linkedin.com/in/alice'
+ 'https://linkedin.com/in/alice' | 'https://linkedin.com/in/alice'
+ 'https://linkedin.com/in/alice/more/path' | 'https://linkedin.com/in/alice/more/path'
+ end
+
+ with_them do
+ it { is_expected.to eq(linkedin_url) }
+ end
+ end
+
+ describe '#twitter_url?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:user) { build_stubbed(:user) }
+
+ subject { helper.twitter_url(user) }
+
+ before do
+ user.twitter = twitter_name
+ end
+
+ where(:twitter_name, :twitter_url) do
+ nil | 'https://twitter.com/'
+ '' | 'https://twitter.com/'
+ 'alice' | 'https://twitter.com/alice'
+ 'http://www.twitter.com/alice' | 'http://www.twitter.com/alice'
+ 'http://twitter.com/alice' | 'http://twitter.com/alice'
+ 'https://www.twitter.com/alice' | 'https://www.twitter.com/alice'
+ 'https://twitter.com/alice' | 'https://twitter.com/alice'
+ 'https://twitter.com/alice/more/path' | 'https://twitter.com/alice/more/path'
+ end
+
+ with_them do
+ it { is_expected.to eq(twitter_url) }
+ end
+ end
+
unless Gitlab.jh?
describe '#promo_host' do
subject { helper.promo_host }
@@ -433,7 +487,8 @@ RSpec.describe ApplicationHelper do
page: 'application',
page_type_id: nil,
find_file: nil,
- group: nil
+ group: nil,
+ group_full_path: nil
}
)
end
@@ -449,7 +504,8 @@ RSpec.describe ApplicationHelper do
page: 'application',
page_type_id: nil,
find_file: nil,
- group: group.path
+ group: group.path,
+ group_full_path: group.full_path
}
)
end
@@ -473,6 +529,7 @@ RSpec.describe ApplicationHelper do
page_type_id: nil,
find_file: nil,
group: nil,
+ group_full_path: nil,
project_id: project.id,
project: project.path,
namespace_id: project.namespace.id
@@ -491,6 +548,7 @@ RSpec.describe ApplicationHelper do
page_type_id: nil,
find_file: nil,
group: project.group.name,
+ group_full_path: project.group.full_path,
project_id: project.id,
project: project.path,
namespace_id: project.namespace.id
@@ -517,6 +575,7 @@ RSpec.describe ApplicationHelper do
page_type_id: issue.id,
find_file: nil,
group: nil,
+ group_full_path: nil,
project_id: issue.project.id,
project: issue.project.path,
namespace_id: issue.project.namespace.id
@@ -590,12 +649,13 @@ RSpec.describe ApplicationHelper do
it 'adds custom form builder to options and calls `form_for`' do
options = { html: { class: 'foo-bar' } }
- expected_options = options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder, url: '/root' })
+ expected_options = options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder })
expect do |b|
helper.gitlab_ui_form_for(user, options, &b)
end.to yield_with_args(::Gitlab::FormBuilders::GitlabUiFormBuilder)
- expect(helper).to have_received(:form_for).with(user, expected_options)
+
+ expect(helper).to have_received(:form_for).with(user, a_hash_including(expected_options))
end
end
@@ -719,22 +779,8 @@ RSpec.describe ApplicationHelper do
end
describe 'stylesheet_link_tag_defer' do
- it 'uses print stylesheet when feature flag disabled' do
- stub_feature_flags(remove_startup_css: false)
-
- expect(helper.stylesheet_link_tag_defer('test')).to eq( '<link rel="stylesheet" media="print" href="/stylesheets/test.css" />')
- end
-
- it 'uses regular stylesheet when feature flag enabled' do
- stub_feature_flags(remove_startup_css: true)
-
- expect(helper.stylesheet_link_tag_defer('test')).to eq( '<link rel="stylesheet" media="all" href="/stylesheets/test.css" />')
- end
-
- it 'uses regular stylesheet when no_startup_css param present' do
- allow(helper.controller).to receive(:params).and_return({ no_startup_css: '' })
-
- expect(helper.stylesheet_link_tag_defer('test')).to eq( '<link rel="stylesheet" media="all" href="/stylesheets/test.css" />')
+ it 'uses media="all" in stylesheet' do
+ expect(helper.stylesheet_link_tag_defer('test')).to eq( '<link rel="stylesheet" href="/stylesheets/test.css" media="all" />')
end
end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index 8a5669867bf..a59f172061e 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -223,4 +223,123 @@ RSpec.describe ButtonHelper do
end
end
end
+
+ describe '#link_button_to', feature_category: :design_system do
+ let(:content) { 'Button content' }
+ let(:href) { '#' }
+ let(:options) { {} }
+
+ RSpec.shared_examples 'basic behavior' do
+ it 'renders a basic link button' do
+ expect(subject.name).to eq('a')
+ expect(subject.classes).to include(*%w[gl-button btn btn-md btn-default])
+ expect(subject.attr('href')).to eq(href)
+ expect(subject.content.strip).to eq(content)
+ end
+
+ describe 'variant option' do
+ let(:options) { { variant: :danger } }
+
+ it 'renders the variant class' do
+ expect(subject.classes).to include('btn-danger')
+ end
+ end
+
+ describe 'category option' do
+ let(:options) { { category: :tertiary } }
+
+ it 'renders the category class' do
+ expect(subject.classes).to include('btn-default-tertiary')
+ end
+ end
+
+ describe 'size option' do
+ let(:options) { { size: :small } }
+
+ it 'renders the small class' do
+ expect(subject.classes).to include('btn-sm')
+ end
+ end
+
+ describe 'block option' do
+ let(:options) { { block: true } }
+
+ it 'renders the block class' do
+ expect(subject.classes).to include('btn-block')
+ end
+ end
+
+ describe 'selected option' do
+ let(:options) { { selected: true } }
+
+ it 'renders the selected class' do
+ expect(subject.classes).to include('selected')
+ end
+ end
+
+ describe 'target option' do
+ let(:options) { { target: '_blank' } }
+
+ it 'renders the target attribute' do
+ expect(subject.attr('target')).to eq('_blank')
+ end
+ end
+
+ describe 'method option' do
+ let(:options) { { method: :post } }
+
+ it 'renders the data-method attribute' do
+ expect(subject.attr('data-method')).to eq('post')
+ end
+ end
+
+ describe 'icon option' do
+ let(:options) { { icon: 'remove' } }
+
+ it 'renders the icon' do
+ icon = subject.at_css('svg.gl-icon')
+ expect(icon.attr('data-testid')).to eq('remove-icon')
+ end
+ end
+
+ describe 'icon only' do
+ let(:content) { nil }
+ let(:options) { { icon: 'remove' } }
+
+ it 'renders the icon-only class' do
+ expect(subject.classes).to include('btn-icon')
+ end
+ end
+
+ describe 'arbitrary html options' do
+ let(:content) { nil }
+ let(:options) { { data: { foo: true }, aria: { labelledby: 'foo' } } }
+
+ it 'renders the attributes' do
+ expect(subject.attr('data-foo')).to eq('true')
+ expect(subject.attr('aria-labelledby')).to eq('foo')
+ end
+ end
+ end
+
+ describe 'without block' do
+ subject do
+ tag = helper.link_button_to content, href, options
+ Nokogiri::HTML.fragment(tag).first_element_child
+ end
+
+ include_examples 'basic behavior'
+ end
+
+ describe 'with block' do
+ subject do
+ tag = helper.link_button_to href, options do
+ content
+ end
+ Nokogiri::HTML.fragment(tag).first_element_child
+ end
+
+ include_examples 'basic behavior'
+ end
+ end
end
diff --git a/spec/helpers/calendar_helper_spec.rb b/spec/helpers/calendar_helper_spec.rb
index 08993dd1dd0..a18ed479465 100644
--- a/spec/helpers/calendar_helper_spec.rb
+++ b/spec/helpers/calendar_helper_spec.rb
@@ -8,7 +8,10 @@ RSpec.describe CalendarHelper do
it "includes the current_user's feed_token" do
current_user = create(:user)
allow(helper).to receive(:current_user).and_return(current_user)
- expect(helper.calendar_url_options).to include feed_token: current_user.feed_token
+
+ feed_token = helper.calendar_url_options[:feed_token]
+ expect(feed_token).to match(Gitlab::Auth::AuthFinders::PATH_DEPENDENT_FEED_TOKEN_REGEX)
+ expect(feed_token).to end_with(current_user.id.to_s)
end
end
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
index a9ab4ab3b67..30cad66af04 100644
--- a/spec/helpers/ci/jobs_helper_spec.rb
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -6,14 +6,19 @@ RSpec.describe Ci::JobsHelper do
describe 'job helper functions' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:job) { create(:ci_build, project: project) }
+ let_it_be(:user) { create(:user) }
before do
helper.instance_variable_set(:@project, project)
helper.instance_variable_set(:@build, job)
+
+ allow(helper)
+ .to receive(:current_user)
+ .and_return(user)
end
it 'returns jobs data' do
- expect(helper.jobs_data).to include({
+ expect(helper.jobs_data(project, job)).to include({
"endpoint" => "/#{project.full_path}/-/jobs/#{job.id}.json",
"project_path" => project.full_path,
"artifact_help_url" => "/help/user/gitlab_com/index.md#gitlab-cicd",
diff --git a/spec/helpers/ci/pipeline_schedules_helper_spec.rb b/spec/helpers/ci/pipeline_schedules_helper_spec.rb
new file mode 100644
index 00000000000..1ba24a08b58
--- /dev/null
+++ b/spec/helpers/ci/pipeline_schedules_helper_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integration do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:pipeline_schedule) { build_stubbed(:ci_pipeline_schedule, project: project, owner: user) }
+ let_it_be(:timezones) { [{ identifier: "Pacific/Honolulu", name: "Hawaii" }] }
+
+ let_it_be(:pipeline_schedule_variable) do
+ build_stubbed(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
+ end
+
+ describe '#js_pipeline_schedules_form_data' do
+ before do
+ allow(helper).to receive(:timezone_data).and_return(timezones)
+ end
+
+ it 'returns pipeline schedule form data' do
+ expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)).to include({
+ full_path: project.full_path,
+ daily_limit: nil,
+ project_id: project.id,
+ schedules_path: pipeline_schedules_path(project),
+ settings_link: project_settings_ci_cd_path(project),
+ timezone_data: timezones.to_json
+ })
+ end
+ end
+end
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index 61583ca1173..00bc38dbd94 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -72,28 +72,6 @@ RSpec.describe Ci::PipelinesHelper do
end
end
- describe 'has_pipeline_badges?' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- subject { helper.has_pipeline_badges?(pipeline) }
-
- context 'when pipeline has a badge' do
- before do
- pipeline.drop!(:config_error)
- end
-
- it 'shows pipeline badges' do
- expect(subject).to eq(true)
- end
- end
-
- context 'when pipeline has no badges' do
- it 'shows pipeline badges' do
- expect(subject).to eq(false)
- end
- end
- end
-
describe '#pipelines_list_data' do
let_it_be(:project) { create(:project) }
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
index a18c82a80ed..a9fbdfbe3ca 100644
--- a/spec/helpers/clusters_helper_spec.rb
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -224,21 +224,6 @@ RSpec.describe ClustersHelper do
subject
end
end
-
- context 'when remove_monitor_metrics FF is disabled' do
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- end
-
- context 'integrations ' do
- let(:tab) { 'integrations' }
-
- it 'renders integrations tab' do
- expect(helper).to receive(:render).with('integrations')
- subject
- end
- end
- end
end
describe '#cluster_type_label' do
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 64735f8b23b..b9316e46d9d 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -68,17 +68,5 @@ RSpec.describe EnvironmentHelper, feature_category: :environment_management do
graphql_etag_key: environment.etag_cache_key
}.to_json)
end
-
- context 'when metrics dashboard feature is available' do
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- end
-
- it 'includes metrics path' do
- expect(Gitlab::Json.parse(subject)).to include(
- 'environment_metrics_path' => project_metrics_dashboard_path(project, environment: environment)
- )
- end
- end
end
end
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 0ebec3ed6d0..b69d6022e70 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -22,7 +22,6 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
expect(metrics_data).to include(
'settings_path' => edit_project_settings_integration_path(project, 'prometheus'),
'clusters_path' => project_clusters_path(project),
- 'metrics_dashboard_base_path' => project_metrics_dashboard_path(project, environment: environment),
'current_environment_name' => environment.name,
'documentation_path' => help_page_path('administration/monitoring/prometheus/index.md'),
'add_dashboard_documentation_path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
@@ -30,7 +29,6 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
'empty_loading_svg_path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),
'empty_no_data_svg_path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
'empty_unable_to_connect_svg_path' => match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
- 'metrics_endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
'deployments_endpoint' => project_environment_deployments_path(project, environment, format: :json),
'default_branch' => 'master',
'project_path' => project_path(project),
@@ -82,30 +80,6 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
it { is_expected.to include('environment_state' => 'stopped') }
end
- context 'when request is from project scoped metrics path' do
- let(:request) { double('request', path: path) }
-
- before do
- allow(helper).to receive(:request).and_return(request)
- end
-
- context '/:namespace/:project/-/metrics' do
- let(:path) { project_metrics_dashboard_path(project) }
-
- it 'uses correct path for metrics_dashboard_base_path' do
- expect(metrics_data['metrics_dashboard_base_path']).to eq(project_metrics_dashboard_path(project))
- end
- end
-
- context '/:namespace/:project/-/metrics/some_custom_dashboard.yml' do
- let(:path) { "#{project_metrics_dashboard_path(project)}/some_custom_dashboard.yml" }
-
- it 'uses correct path for metrics_dashboard_base_path' do
- expect(metrics_data['metrics_dashboard_base_path']).to eq(project_metrics_dashboard_path(project))
- end
- end
- end
-
context 'when metrics dashboard feature is unavailable' do
before do
stub_feature_flags(remove_monitor_metrics: true)
@@ -147,13 +121,4 @@ RSpec.describe EnvironmentsHelper, feature_category: :environment_management do
expect(helper.environment_logs_data(project, environment)).to eq(expected_data)
end
end
-
- describe '#environment_data' do
- it 'returns the environment as JSON' do
- expected_data = { id: environment.id,
- name: environment.name,
- external_url: environment.external_url }.to_json
- expect(helper.environment_data(environment)).to eq(expected_data)
- end
- end
end
diff --git a/spec/helpers/feed_token_helper_spec.rb b/spec/helpers/feed_token_helper_spec.rb
new file mode 100644
index 00000000000..4382758965c
--- /dev/null
+++ b/spec/helpers/feed_token_helper_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FeedTokenHelper, feature_category: :system_access do
+ describe '#generate_feed_token' do
+ context 'with type :atom' do
+ let(:current_user) { build(:user, feed_token: 'KNOWN VALUE') }
+
+ it "returns the current_user's atom feed_token" do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:current_request).and_return(instance_double(ActionDispatch::Request, path: 'url'))
+
+ expect(helper.generate_feed_token(:atom))
+ # The middle part is the output of OpenSSL::HMAC.hexdigest("SHA256", 'KNOWN VALUE', 'url.atom')
+ .to eq("glft-a8cc74ccb0de004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693-#{current_user.id}")
+ end
+ end
+
+ context 'when signed out' do
+ it "returns nil" do
+ allow(helper).to receive(:current_user).and_return(nil)
+
+ expect(helper.generate_feed_token(:atom)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index bdcf0ef57ee..1b5f23a5e8e 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -421,6 +421,20 @@ RSpec.describe GroupsHelper do
end
end
+ describe '#can_admin_service_accounts?', feature_category: :user_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before do
+ allow(helper).to receive(:current_user) { user }
+ group.add_owner(user)
+ end
+
+ it 'returns false when current_user can not admin members' do
+ expect(helper.can_admin_service_accounts?(group)).to be(false)
+ end
+ end
+
describe '#localized_jobs_to_be_done_choices' do
it 'has a translation for all `jobs_to_be_done` values' do
expect(localized_jobs_to_be_done_choices.keys).to match_array(NamespaceSetting.jobs_to_be_dones.keys)
@@ -489,6 +503,7 @@ RSpec.describe GroupsHelper do
it 'returns expected hash' do
expect(helper.group_overview_tabs_app_data(group)).to match(
{
+ group_id: group.id,
subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb
index ac4f882f872..f481611b2a2 100644
--- a/spec/helpers/integrations_helper_spec.rb
+++ b/spec/helpers/integrations_helper_spec.rb
@@ -275,7 +275,7 @@ RSpec.describe IntegrationsHelper, feature_category: :integrations do
with_them do
before do
- issue.assign_attributes(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
+ issue.assign_attributes(work_item_type: WorkItems::Type.default_by_type(issue_type))
issue.save!(validate: false)
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index ffaffa251d1..a2b8ee061bb 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -666,9 +666,11 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
describe '#sidebar_milestone_tooltip_label' do
it 'escapes HTML in the milestone title' do
- milestone = build(:milestone, title: '&lt;img onerror=alert(1)&gt;')
+ milestone = build(:milestone, title: '&lt;img onerror=alert(1)&gt;', due_date: Date.new(2022, 6, 26))
- expect(helper.sidebar_milestone_tooltip_label(milestone)).to eq('&lt;img onerror=alert(1)&gt;<br/>Milestone')
+ expect(helper.sidebar_milestone_tooltip_label(milestone)).to eq(
+ '&lt;img onerror=alert(1)&gt;<br/>Jun 26, 2022 (<strong>Past due</strong>)'
+ )
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 38cbb5a1d66..ba323140720 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -364,7 +364,8 @@ RSpec.describe IssuesHelper do
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
new_project_path: new_project_path(namespace_id: group.id),
rss_path: '#',
- sign_in_path: new_user_session_path
+ sign_in_path: new_user_session_path,
+ group_id: group.id
}
expect(helper.group_issues_list_data(group, current_user)).to include(expected)
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index a9f99f29f6d..562d6683d97 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe MarkupHelper do
it 'ignores reference links when they are the entire body' do
text = issues[0].to_reference
act = helper.link_to_markdown(text, '/foo')
- expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
+ expect(act).to eq %(<a href="/foo">#{issues[0].to_reference}</a>)
end
it 'replaces commit message with emoji to link' do
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 5ae057dc97d..26dadd3b4f1 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_project',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- qa_selector: 'global_new_project_link'
+ testid: 'global_new_project_link'
}
)
)
@@ -104,7 +104,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_group',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- qa_selector: 'global_new_group_link'
+ testid: 'global_new_group_link'
}
)
)
@@ -127,7 +127,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_snippet_parent',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- qa_selector: 'global_new_snippet_link'
+ testid: 'global_new_snippet_link'
}
)
)
@@ -256,7 +256,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_issue',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- qa_selector: 'new_issue_link'
+ testid: 'new_issue_link'
}
)
)
@@ -340,7 +340,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_issue',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- qa_selector: 'new_issue_link'
+ testid: 'new_issue_link'
}
)
)
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index 252423aa988..6ffc2cbf694 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe Nav::TopNavHelper do
track_action: 'click_dropdown',
track_label: 'projects_dropdown',
track_property: 'navigation_top',
- qa_selector: 'projects_dropdown'
+ testid: 'projects_dropdown'
},
icon: 'project',
id: 'project',
@@ -166,7 +166,7 @@ RSpec.describe Nav::TopNavHelper do
expected_links_primary = [
::Gitlab::Nav::TopNavMenuItem.build(
data: {
- qa_selector: 'menu_item_link',
+ testid: 'menu_item_link',
qa_title: 'View all projects',
**menu_data_tracking_attrs('view_all_projects')
},
@@ -231,7 +231,7 @@ RSpec.describe Nav::TopNavHelper do
track_action: 'click_dropdown',
track_label: 'groups_dropdown',
track_property: 'navigation_top',
- qa_selector: 'groups_dropdown'
+ testid: 'groups_dropdown'
},
icon: 'group',
id: 'groups',
@@ -264,7 +264,7 @@ RSpec.describe Nav::TopNavHelper do
expected_links_primary = [
::Gitlab::Nav::TopNavMenuItem.build(
data: {
- qa_selector: 'menu_item_link',
+ testid: 'menu_item_link',
qa_title: 'View all groups',
**menu_data_tracking_attrs('view_all_groups')
},
@@ -408,7 +408,7 @@ RSpec.describe Nav::TopNavHelper do
it 'has enter_admin_mode as last :secondary item' do
expected_enter_admin_mode_item = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
- qa_selector: 'menu_item_link',
+ testid: 'menu_item_link',
qa_title: 'Enter admin mode',
**menu_data_tracking_attrs('enter_admin_mode')
},
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index ae8a7f0c14c..6d6b8e4c707 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -132,7 +132,6 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do
describe '#show_container_registry_settings' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
- let_it_be(:admin) { create(:admin) }
before do
allow(helper).to receive(:current_user) { user }
@@ -252,4 +251,114 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do
end
end
end
+
+ describe '#can_delete_packages?' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ subject { helper.can_delete_packages?(project) }
+
+ context 'with package registry config enabled' do
+ before do
+ stub_config(packages: { enabled: true })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'with package registry config disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+ end
+
+ describe '#can_delete_group_packages?' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ subject { helper.can_delete_group_packages?(group) }
+
+ context 'with package registry config enabled' do
+ before do
+ stub_config(packages: { enabled: true })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'with package registry config disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ context 'when user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when user does not have permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+ end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index b14789fd5d2..43500d98591 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -270,7 +270,7 @@ RSpec.describe PageLayoutHelper do
it 'merges the status properties with the defaults' do
is_expected.to eq({
- current_clear_status_after: time.to_s(:iso8601),
+ current_clear_status_after: time.to_fs(:iso8601),
current_availability: 'busy',
current_emoji: 'basketball',
current_message: 'Some message',
diff --git a/spec/helpers/projects/observability_helper_spec.rb b/spec/helpers/projects/observability_helper_spec.rb
new file mode 100644
index 00000000000..65b6ddf04ec
--- /dev/null
+++ b/spec/helpers/projects/observability_helper_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'json'
+
+RSpec.describe Projects::ObservabilityHelper, type: :helper, feature_category: :tracing do
+ describe '#observability_tracing_view_model' do
+ let_it_be(:group) { build_stubbed(:group) }
+ let_it_be(:project) { build_stubbed(:project, group: group) }
+
+ it 'generates the correct JSON' do
+ expected_json = {
+ tracingUrl: Gitlab::Observability.tracing_url(project),
+ provisioningUrl: Gitlab::Observability.provisioning_url(project),
+ oauthUrl: Gitlab::Observability.oauth_url
+ }.to_json
+
+ expect(helper.observability_tracing_view_model(project)).to eq(expected_json)
+ end
+ end
+end
diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb
index a69da915990..baeafe6b7e7 100644
--- a/spec/helpers/projects/pipeline_helper_spec.rb
+++ b/spec/helpers/projects/pipeline_helper_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Projects::PipelineHelper do
failed: pipeline.failure_reason?.to_s,
auto_devops: pipeline.auto_devops_source?.to_s,
detached: pipeline.detached_merge_request_pipeline?.to_s,
- stuck: pipeline.stuck?,
+ stuck: pipeline.stuck?.to_s,
ref_text: pipeline.ref_text
})
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index cde7fc0e272..768038d8736 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
before do
+ allow(helper).to receive(:current_user).and_return(user)
helper.instance_variable_set(:@project, project)
end
@@ -143,7 +144,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
let(:project) { project_with_repo }
before do
- allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :read_cross_project) { true }
allow(user).to receive(:max_member_access_for_project).and_return(40)
allow(Gitlab::I18n).to receive(:locale).and_return('es')
@@ -287,10 +287,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
describe '#show_no_ssh_key_message?' do
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
context 'user has no keys' do
it 'returns true' do
expect(helper.show_no_ssh_key_message?).to be_truthy
@@ -307,10 +303,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
describe '#show_no_password_message?' do
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
context 'user has password set' do
it 'returns false' do
expect(helper.show_no_password_message?).to be_falsey
@@ -346,10 +338,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
describe '#no_password_message' do
let(:user) { create(:user, password_automatically_set: true) }
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
context 'password authentication is enabled for Git' do
it 'returns message prompting user to set password or set up a PAT' do
stub_application_setting(password_authentication_enabled_for_git?: true)
@@ -431,10 +419,10 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
describe 'default_clone_protocol' do
+ let(:user) { nil }
+
context 'when user is not logged in and gitlab protocol is HTTP' do
it 'returns HTTP' do
- allow(helper).to receive(:current_user).and_return(nil)
-
expect(helper.send(:default_clone_protocol)).to eq('http')
end
end
@@ -442,7 +430,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
context 'when user is not logged in and gitlab protocol is HTTPS' do
it 'returns HTTPS' do
stub_config_setting(protocol: 'https')
- allow(helper).to receive(:current_user).and_return(nil)
expect(helper.send(:default_clone_protocol)).to eq('https')
end
@@ -453,10 +440,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
let(:user) { double(:user, fork_of: nil) }
let(:project) { double(:project, id: 1) }
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
context 'when there is no current_user' do
let(:user) { nil }
@@ -543,10 +526,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
describe '#git_user_name' do
let(:user) { build_stubbed(:user, name: 'John "A" Doe53') }
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
it 'parses quotes in name' do
expect(helper.send(:git_user_name)).to eq('John \"A\" Doe53')
end
@@ -554,9 +533,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
describe '#git_user_email' do
context 'not logged-in' do
- before do
- allow(helper).to receive(:current_user).and_return(nil)
- end
+ let(:user) { nil }
it 'returns your@email.com' do
expect(helper.send(:git_user_email)).to eq('your@email.com')
@@ -564,10 +541,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
context 'user logged in' do
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
context 'user has no configured commit email' do
it 'returns the primary email' do
expect(helper.send(:git_user_email)).to eq(user.email)
@@ -807,9 +780,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
describe '#can_admin_project_member?' do
context 'when user is project owner' do
- before do
- allow(helper).to receive(:current_user) { project.owner }
- end
+ let(:user) { project.owner }
it 'returns true for owner of project' do
expect(helper.can_admin_project_member?(project)).to eq true
@@ -829,7 +800,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
with_them do
before do
project.add_role(user, user_project_role)
- allow(helper).to receive(:current_user) { user }
end
it 'resolves if the user can import members' do
@@ -1016,7 +986,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
before do
allow(helper).to receive(:can?) { true }
- allow(helper).to receive(:current_user).and_return(user)
end
it 'includes project_permissions_settings' do
@@ -1188,10 +1157,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
shared_examples 'configure import method modal' do
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
context 'as a user' do
it 'returns a link to contact an administrator' do
allow(user).to receive(:can_admin_all_resources?).and_return(false)
@@ -1290,16 +1255,14 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
describe '#can_admin_associated_clusters?' do
- let_it_be(:current_user) { create(:user) }
let_it_be_with_reload(:project) { create(:project) }
subject { helper.send(:can_admin_associated_clusters?, project) }
before do
- allow(helper).to receive(:current_user).and_return(current_user)
allow(helper)
.to receive(:can?)
- .with(current_user, :admin_cluster, namespace)
+ .with(user, :admin_cluster, namespace)
.and_return(user_can_admin_cluster)
end
@@ -1394,7 +1357,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}. Contact GitLab Support if you have any additional questions.'))
+ expect(subject).to eq(s_('ClusterIntegration|The certificate-based Kubernetes integration is deprecated and will be removed in the future. You should %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}. For more information, see the %{deprecationLinkStart}deprecation epic%{deprecationLinkEnd}, or contact GitLab support.'))
end
end
@@ -1402,7 +1365,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
let_it_be(:has_active_license) { false }
it 'displays the correct message' do
- expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of February 2023. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.'))
+ expect(subject).to eq(s_('ClusterIntegration|The certificate-based Kubernetes integration is deprecated and will be removed in the future. You should %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}. For more information, see the %{deprecationLinkStart}deprecation epic%{deprecationLinkEnd}.'))
end
end
end
@@ -1477,8 +1440,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
it 'returns the data related to fork divergence' do
- allow(helper).to receive(:current_user).and_return(user)
-
ahead_path =
"/#{project.full_path}/-/compare/#{source_project.default_branch}...ref?from_project_id=#{source_project.id}"
behind_path =
@@ -1500,8 +1461,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
it 'returns view_mr_path if a merge request for the branch exists' do
- allow(helper).to receive(:current_user).and_return(user)
-
merge_request =
create(:merge_request, source_project: project, target_project: project_with_repo,
source_branch: project.default_branch, target_branch: project_with_repo.default_branch)
@@ -1523,8 +1482,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
with_them do
it 'create_mr_path is nil' do
- allow(helper).to receive(:current_user).and_return(user)
-
project.add_member(user, project_role)
source_project.add_member(user, source_project_role)
@@ -1562,4 +1519,99 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
it { expect(subject).to eq('ssh_url_to_repo') }
end
+
+ describe '#can_view_branch_rules?' do
+ subject { helper.can_view_branch_rules? }
+
+ context 'when user is a maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#can_push_code?' do
+ subject { helper.can_push_code? }
+
+ context 'when user is nil' do
+ let(:user) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when user is a developer on the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user is a reporter on the project' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#can_admin_associated_clusters?(project)' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_clusters_exist, :user_can_admin_project_clusters, :group_clusters_exist, :user_can_admin_group_clusters, :expected) do
+ false | false | false | false | false
+ true | false | false | false | false
+ false | true | false | false | false
+ false | false | true | false | false
+ false | false | false | true | false
+ true | true | false | false | true
+ false | false | true | true | true
+ true | true | true | true | true
+ end
+
+ with_them do
+ subject { helper.can_admin_associated_clusters?(project) }
+
+ let(:clusters) { [double('Cluster')] }
+ let(:group) { double('Group') }
+
+ before do
+ allow(project)
+ .to receive(:clusters)
+ .and_return(project_clusters_exist ? clusters : [])
+ allow(helper)
+ .to receive(:can?).with(user, :admin_cluster, project)
+ .and_return(user_can_admin_project_clusters)
+
+ allow(project)
+ .to receive(:group)
+ .and_return(group)
+ allow(group)
+ .to receive(:clusters)
+ .and_return(group_clusters_exist ? clusters : [])
+ allow(helper)
+ .to receive(:can?).with(user, :admin_cluster, project.group)
+ .and_return(user_can_admin_group_clusters)
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+
+ describe '#branch_rules_path' do
+ subject { helper.branch_rules_path }
+
+ it { is_expected.to eq(project_settings_repository_path(project, anchor: 'js-branch-rules')) }
+ end
end
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
index 05f6ebb6c1b..f99a1f6d547 100644
--- a/spec/helpers/rss_helper_spec.rb
+++ b/spec/helpers/rss_helper_spec.rb
@@ -8,7 +8,10 @@ RSpec.describe RssHelper do
it "includes the current_user's feed_token" do
current_user = create(:user)
allow(helper).to receive(:current_user).and_return(current_user)
- expect(helper.rss_url_options).to include feed_token: current_user.feed_token
+
+ feed_token = helper.rss_url_options[:feed_token]
+ expect(feed_token).to match(Gitlab::Auth::AuthFinders::PATH_DEPENDENT_FEED_TOKEN_REGEX)
+ expect(feed_token).to end_with(current_user.id.to_s)
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index b2606fcfae1..1ff7e48abfc 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -153,15 +153,7 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
end
- [true, false].each do |enabled|
- context "with feature flag autcomplete_users_use_search_service #{enabled}" do
- before do
- stub_feature_flags(autocomplete_users_use_search_service: enabled)
- end
-
- include_examples 'for users'
- end
- end
+ include_examples 'for users'
it "includes the required project attrs" do
project = create(:project, namespace: create(:namespace, owner: user))
@@ -730,78 +722,6 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
end
- describe '#show_user_search_tab?' do
- subject { show_user_search_tab? }
-
- let(:current_user) { build(:user) }
-
- before do
- allow(self).to receive(:current_user).and_return(current_user)
- end
-
- context 'when project search' do
- before do
- @project = :some_project
-
- expect(self).to receive(:project_search_tabs?)
- .with(:users)
- .and_return(:value)
- end
-
- it 'delegates to project_search_tabs?' do
- expect(subject).to eq(:value)
- end
- end
-
- context 'when group search' do
- before do
- @group = :some_group
- end
-
- context 'when current_user can read_users_list' do
- before do
- allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(true)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when current_user cannot read_users_list' do
- before do
- allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when global search' do
- context 'when current_user can read_users_list' do
- before do
- allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(true)
- end
-
- it { is_expected.to eq(true) }
-
- context 'when global_search_user_tab feature flag is disabled' do
- before do
- stub_feature_flags(global_search_users_tab: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when current_user cannot read_users_list' do
- before do
- allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
- end
-
describe '#repository_ref' do
using RSpec::Parameterized::TableSyntax
@@ -1121,248 +1041,24 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
end
- describe '.search_navigation' do
- using RSpec::Parameterized::TableSyntax
- let(:user) { build(:user) }
- let_it_be(:project) { build(:project) }
-
- before do
- allow(self).to receive(:current_user).and_return(user)
- allow(self).to receive(:can?).and_return(true)
- allow(self).to receive(:project_search_tabs?).and_return(false)
- allow(self).to receive(:feature_flag_tab_enabled?).and_return(false)
- end
-
- context 'projects' do
- where(:global_project, :condition) do
- nil | true
- ref(:project) | false
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
-
- expect(search_navigation[:projects][:condition]).to eq(condition)
- end
- end
- end
-
- context 'code' do
- where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do
- false | false | nil | false | false
- true | true | nil | true | true
- true | false | nil | false | false
- false | true | nil | false | false
- false | false | ref(:project) | true | true
- true | false | ref(:project) | false | false
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_code_tab).and_return(feature_flag_tab_enabled)
- allow(self).to receive(:project_search_tabs?).with(:blobs).and_return(project_search_tabs)
-
- expect(search_navigation[:blobs][:condition]).to eq(condition)
- end
- end
- end
-
- context 'issues' do
- where(:project_search_tabs, :global_search_issues_tab, :global_project, :condition) do
- false | false | nil | false
- false | true | nil | true
- false | true | ref(:project) | false
- false | false | ref(:project) | false
- true | false | nil | true
- true | true | nil | true
- true | false | ref(:project) | true
- true | true | ref(:project) | true
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_issues_tab).and_return(global_search_issues_tab)
- allow(self).to receive(:project_search_tabs?).with(:issues).and_return(project_search_tabs)
-
- expect(search_navigation[:issues][:condition]).to eq(condition)
- end
- end
- end
-
- context 'merge requests' do
- where(:project_search_tabs, :feature_flag_tab_enabled, :global_project, :condition) do
- false | false | nil | false
- true | false | nil | true
- false | false | ref(:project) | false
- true | false | ref(:project) | true
- false | true | nil | true
- true | true | nil | true
- false | true | ref(:project) | false
- true | true | ref(:project) | true
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_merge_requests_tab).and_return(feature_flag_tab_enabled)
- allow(self).to receive(:project_search_tabs?).with(:merge_requests).and_return(project_search_tabs)
-
- expect(search_navigation[:merge_requests][:condition]).to eq(condition)
- end
- end
- end
-
- context 'wiki' do
- where(:global_search_wiki_tab, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do
- false | false | nil | true | true
- false | false | nil | false | false
- false | false | ref(:project) | false | false
- false | true | nil | false | false
- false | true | ref(:project) | false | false
- true | false | nil | false | false
- true | true | ref(:project) | false | false
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_wiki_tab).and_return(global_search_wiki_tab)
- allow(self).to receive(:project_search_tabs?).with(:wiki_blobs).and_return(project_search_tabs)
-
- expect(search_navigation[:wiki_blobs][:condition]).to eq(condition)
- end
- end
- end
-
- context 'commits' do
- where(:global_search_commits_tab, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do
- false | false | nil | true | true
- false | false | ref(:project) | true | true
- false | false | nil | false | false
- false | true | ref(:project) | false | false
- false | true | nil | false | false
- true | false | nil | false | false
- true | false | ref(:project) | false | false
- true | true | ref(:project) | false | false
- true | true | nil | false | true
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_commits_tab).and_return(global_search_commits_tab)
- allow(self).to receive(:project_search_tabs?).with(:commits).and_return(project_search_tabs)
-
- expect(search_navigation[:commits][:condition]).to eq(condition)
- end
- end
- end
-
- context 'comments' do
- where(:project_search_tabs, :show_elasticsearch_tabs, :global_project, :condition) do
- true | true | nil | true
- true | true | ref(:project) | true
- false | false | nil | false
- false | false | ref(:project) | false
- false | true | nil | true
- false | true | ref(:project) | false
- true | false | nil | true
- true | false | ref(:project) | true
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
- allow(self).to receive(:project_search_tabs?).with(:notes).and_return(project_search_tabs)
-
- expect(search_navigation[:notes][:condition]).to eq(condition)
- end
- end
- end
-
- context 'milestones' do
- where(:global_project, :project_search_tabs, :condition) do
- ref(:project) | true | true
- nil | false | true
- ref(:project) | false | false
- nil | true | true
- end
-
- with_them do
- it 'data item condition is set correctly' do
- @project = global_project
- allow(self).to receive(:project_search_tabs?).with(:milestones).and_return(project_search_tabs)
-
- expect(search_navigation[:milestones][:condition]).to eq(condition)
- end
- end
- end
-
- context 'users' do
- where(:show_user_search_tab, :condition) do
- true | true
- false | false
- end
-
- with_them do
- it 'data item condition is set correctly' do
- allow(self).to receive(:show_user_search_tab?).and_return(show_user_search_tab)
-
- expect(search_navigation[:users][:condition]).to eq(condition)
- end
- end
- end
-
- context 'snippet_titles' do
- where(:global_project, :global_show_snippets, :global_feature_flag_enabled, :condition) do
- ref(:project) | true | false | false
- nil | false | false | false
- ref(:project) | false | false | false
- nil | true | false | false
- ref(:project) | true | true | false
- nil | false | true | false
- ref(:project) | false | true | false
- nil | true | true | true
- end
-
- with_them do
- it 'data item condition is set correctly' do
- allow(search_service).to receive(:show_snippets?).and_return(global_show_snippets)
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_snippet_titles_tab)
- .and_return(global_feature_flag_enabled)
- @project = global_project
-
- expect(search_navigation[:snippet_titles][:condition]).to eq(condition)
- end
- end
- end
- end
-
describe '.search_navigation_json' do
using RSpec::Parameterized::TableSyntax
- context 'with data' do
+ context 'with some tab conditions set to false' do
example_data_1 = {
projects: { label: _("Projects"), condition: true },
- blobs: { label: _("Code"), condition: false }
+ blobs: { label: _("Code"), condition: false }
}
example_data_2 = {
projects: { label: _("Projects"), condition: false },
- blobs: { label: _("Code"), condition: false }
+ blobs: { label: _("Code"), condition: false }
}
example_data_3 = {
projects: { label: _("Projects"), condition: true },
- blobs: { label: _("Code"), condition: true },
- epics: { label: _("Epics"), condition: true }
+ blobs: { label: _("Code"), condition: true },
+ epics: { label: _("Epics"), condition: true }
}
where(:data, :matcher) do
@@ -1373,28 +1069,31 @@ RSpec.describe SearchHelper, feature_category: :global_search do
with_them do
it 'renders data correctly' do
- allow(self).to receive(:search_navigation).with(no_args).and_return(data)
+ allow(self).to receive(:current_user).and_return(build(:user))
+ allow_next_instance_of(Search::Navigation) do |search_nav|
+ allow(search_nav).to receive(:tabs).and_return(data)
+ end
expect(search_navigation_json).to instance_exec(&matcher)
end
end
end
- end
- describe '.search_navigation_json with .search_navigation' do
- before do
- allow(self).to receive(:current_user).and_return(build(:user))
- allow(self).to receive(:can?).and_return(true)
- allow(self).to receive(:project_search_tabs?).and_return(true)
- allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
- allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true)
- allow(search_service).to receive(:show_snippets?).and_return(true)
- @project = nil
- end
+ context 'when all options enabled' do
+ before do
+ allow(self).to receive(:current_user).and_return(build(:user))
+ allow(search_service).to receive(:show_snippets?).and_return(true)
+ allow_next_instance_of(Search::Navigation) do |search_nav|
+ allow(search_nav).to receive(:tab_enabled_for_project?).and_return(true)
+ allow(search_nav).to receive(:feature_flag_tab_enabled?).and_return(true)
+ end
+
+ @project = nil
+ end
- it 'test search navigation item order for CE all options enabled' do
- expect(Gitlab::Json.parse(search_navigation_json).keys).to eq(%w[projects blobs issues merge_requests wiki_blobs commits notes milestones users snippet_titles])
+ it 'returns items in order' do
+ expect(Gitlab::Json.parse(search_navigation_json).keys).to eq(%w[projects blobs issues merge_requests wiki_blobs commits notes milestones users snippet_titles])
+ end
end
end
@@ -1404,9 +1103,9 @@ RSpec.describe SearchHelper, feature_category: :global_search do
context 'data' do
where(:scope, :label, :data, :search, :active_scope) do
"projects" | "Projects" | { qa_selector: 'projects_tab' } | nil | "projects"
- "snippet_titles" | "Titles and Descriptions" | nil | { snippets: "test" } | "code"
+ "snippet_titles" | "Snippets" | nil | { snippets: "test" } | "code"
"projects" | "Projects" | { qa_selector: 'projects_tab' } | nil | "issue"
- "snippet_titles" | "Titles and Descriptions" | nil | { snippets: "test" } | "snippet_titles"
+ "snippet_titles" | "Snippets" | nil | { snippets: "test" } | "snippet_titles"
end
with_them do
@@ -1439,27 +1138,12 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
end
- describe 'show_elasticsearch_tabs' do
- subject { search_service.show_elasticsearch_tabs? }
-
- let(:user) { build(:user) }
-
- before do
- allow(self).to receive(:current_user).and_return(user)
- end
-
- it { is_expected.to eq(false) }
- end
-
- describe 'show_epics' do
- subject { search_service.show_epics? }
-
- let(:user) { build(:user) }
-
- before do
- allow(self).to receive(:current_user).and_return(user)
+ describe '#wiki_blob_link' do
+ let_it_be(:project) { create :project, :wiki_repo }
+ let(:wiki_blob) do
+ Gitlab::Search::FoundBlob.new({ path: 'test', basename: 'test', ref: 'master', data: 'foo', startline: 2, project: project, project_id: project.id })
end
- it { is_expected.to eq(false) }
+ it { expect(wiki_blob_link(wiki_blob)).to eq("/#{project.namespace.path}/#{project.path}/-/wikis/#{wiki_blob.path}") }
end
end
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 6648663b634..8d8bbcd2737 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -106,7 +106,8 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
customized: user.status&.customized?,
availability: user.status&.availability.to_s,
emoji: user.status&.emoji,
- message: user.status&.message_html&.html_safe,
+ message_html: user.status&.message_html&.html_safe,
+ message: user.status&.message&.html_safe,
clear_after: nil
},
settings: {
@@ -250,7 +251,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
"data-track-label": id,
"data-track-action": "click_link",
"data-track-property": "nav_create_menu",
- "data-qa-selector": 'create_menu_item',
+ "data-testid": 'create_menu_item',
"data-qa-create-menu-item": id
}
}
@@ -279,7 +280,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
"data-track-label": id,
"data-track-action": "click_link",
"data-track-property": "nav_create_menu",
- "data-qa-selector": 'create_menu_item',
+ "data-testid": 'create_menu_item',
"data-qa-create-menu-item": id
}
}
@@ -482,6 +483,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
let(:user) { build(:user) }
let(:group) { build(:group) }
let(:project) { build(:project) }
+ let(:organization) { build(:organization) }
before do
allow(helper).to receive(:project_sidebar_context_data).and_return(
@@ -516,6 +518,12 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
expect(helper.super_sidebar_nav_panel(nav: 'admin')).to be_a(Sidebars::Admin::Panel)
end
+ it 'returns Organization Panel for organization nav' do
+ expect(
+ helper.super_sidebar_nav_panel(nav: 'organization', organization: organization)
+ ).to be_a(Sidebars::Organizations::SuperSidebarPanel)
+ 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
@@ -528,4 +536,31 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
expect(helper.super_sidebar_nav_panel(user: user)).to be_a(Sidebars::YourWork::Panel)
end
end
+
+ describe '#command_palette_data' do
+ it 'returns data for project files search' do
+ project = create(:project, :repository) # rubocop:disable RSpec/FactoryBot/AvoidCreate
+
+ expect(helper.command_palette_data(project: project)).to eq(
+ project_files_url: project_files_path(
+ project, project.default_branch, format: :json),
+ project_blob_url: project_blob_path(
+ project, project.default_branch)
+ )
+ end
+
+ it 'returns empty object when project is nil' do
+ expect(helper.command_palette_data(project: nil)).to eq({})
+ end
+
+ it 'returns empty object when project does not have repo' do
+ project = build(:project)
+ expect(helper.command_palette_data(project: project)).to eq({})
+ end
+
+ it 'returns empty object when project has repo but it is empty' do
+ project = build(:project, :empty_repo)
+ expect(helper.command_palette_data(project: project)).to eq({})
+ end
+ end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index e13b83feefd..1ca5b8eb954 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -274,6 +274,7 @@ RSpec.describe TreeHelper do
describe '.fork_modal_options' do
let_it_be(:blob) { project.repository.blob_at('refs/heads/master', @path) }
+ let(:fork_path) { "/#{project.path_with_namespace}/-/forks/new" }
before do
allow(helper).to receive(:current_user).and_return(user)
@@ -282,7 +283,7 @@ RSpec.describe TreeHelper do
subject { helper.fork_modal_options(project, blob) }
it 'returns correct fork path' do
- expect(subject).to match a_hash_including(fork_path: '/namespace1/project-1/-/forks/new', fork_modal_id: nil)
+ expect(subject).to match a_hash_including(fork_path: fork_path, fork_modal_id: nil)
end
context 'when show_edit_button true' do
@@ -292,7 +293,7 @@ RSpec.describe TreeHelper do
it 'returns correct fork path and modal id' do
expect(subject).to match a_hash_including(
- fork_path: '/namespace1/project-1/-/forks/new',
+ fork_path: fork_path,
fork_modal_id: 'modal-confirm-fork-edit')
end
end
@@ -304,7 +305,7 @@ RSpec.describe TreeHelper do
it 'returns correct fork path and modal id' do
expect(subject).to match a_hash_including(
- fork_path: '/namespace1/project-1/-/forks/new',
+ fork_path: fork_path,
fork_modal_id: 'modal-confirm-fork-webide')
end
end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 6ee208dfd15..c0d3c31a36d 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -496,13 +496,17 @@ RSpec.describe UsersHelper do
describe '#user_profile_tabs_app_data' do
before do
+ allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:user_calendar_path).with(user, :json).and_return('/users/root/calendar.json')
allow(helper).to receive(:user_activity_path).with(user, :json).and_return('/users/root/activity.json')
+ allow(helper).to receive(:new_snippet_path).and_return('/-/snippets/new')
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
+ allow(helper).to receive(:can?).with(user, :create_snippet).and_return(true)
+
expect(helper.user_profile_tabs_app_data(user)).to match({
followees_count: 3,
followers_count: 2,
@@ -510,9 +514,21 @@ RSpec.describe UsersHelper do
user_activity_path: '/users/root/activity.json',
utc_offset: 0,
user_id: user.id,
- snippets_empty_state: match_asset_path('illustrations/empty-state/empty-snippets-md.svg')
+ new_snippet_path: '/-/snippets/new',
+ snippets_empty_state: match_asset_path('illustrations/empty-state/empty-snippets-md.svg'),
+ follow_empty_state: match_asset_path('illustrations/empty-state/empty-friends-md.svg')
})
end
+
+ context 'when user does not have create_snippet permissions' do
+ before do
+ allow(helper).to receive(:can?).with(user, :create_snippet).and_return(false)
+ end
+
+ it 'returns nil for new_snippet_path property' do
+ expect(helper.user_profile_tabs_app_data(user)[:new_snippet_path]).to be_nil
+ end
+ end
end
describe '#load_max_project_member_accesses' do
diff --git a/spec/helpers/web_hooks/web_hooks_helper_spec.rb b/spec/helpers/web_hooks/web_hooks_helper_spec.rb
index 5c68a436ad2..ccee2c2508d 100644
--- a/spec/helpers/web_hooks/web_hooks_helper_spec.rb
+++ b/spec/helpers/web_hooks/web_hooks_helper_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe WebHooks::WebHooksHelper, :clean_gitlab_redis_shared_state, featu
include_context 'a hook has failed'
it 'is true' do
- expect(helper).to be_show_project_hook_failed_callout(project: project)
+ expect(helper.show_project_hook_failed_callout?(project: project)).to eq(true)
end
it 'stores a value' do
@@ -64,7 +64,7 @@ RSpec.describe WebHooks::WebHooksHelper, :clean_gitlab_redis_shared_state, featu
contexts.each { |ctx| include_context(ctx) unless ctx == name }
it 'is false' do
- expect(helper).not_to be_show_project_hook_failed_callout(project: project)
+ expect(helper.show_project_hook_failed_callout?(project: project)).to eq(false)
end
end
end
diff --git a/spec/initializers/00_rails_disable_joins_spec.rb b/spec/initializers/00_rails_disable_joins_spec.rb
deleted file mode 100644
index 3b390f1ef17..00000000000
--- a/spec/initializers/00_rails_disable_joins_spec.rb
+++ /dev/null
@@ -1,288 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'DisableJoins' do
- let(:primary_model) do
- Class.new(ApplicationRecord) do
- self.table_name = '_test_primary_records'
-
- def self.name
- 'TestPrimary'
- end
- end
- end
-
- let(:bridge_model) do
- Class.new(ApplicationRecord) do
- self.table_name = '_test_bridge_records'
-
- def self.name
- 'TestBridge'
- end
- end
- end
-
- let(:secondary_model) do
- Class.new(ApplicationRecord) do
- self.table_name = '_test_secondary_records'
-
- def self.name
- 'TestSecondary'
- end
- end
- end
-
- context 'passing disable_joins as an association option' do
- context 'when the association is a bare has_one' do
- it 'disallows the disable_joins option' do
- expect do
- primary_model.has_one :test_bridge, disable_joins: true
- end.to raise_error(ArgumentError, /Unknown key: :disable_joins/)
- end
- end
-
- context 'when the association is a belongs_to' do
- it 'disallows the disable_joins option' do
- expect do
- bridge_model.belongs_to :test_secondary, disable_joins: true
- end.to raise_error(ArgumentError, /Unknown key: :disable_joins/)
- end
- end
-
- context 'when the association is has_one :through' do
- it 'allows the disable_joins option' do
- primary_model.has_one :test_bridge
- bridge_model.belongs_to :test_secondary
-
- expect do
- primary_model.has_one :test_secondary, through: :test_bridge, disable_joins: true
- end.not_to raise_error
- end
- end
-
- context 'when the association is a bare has_many' do
- it 'disallows the disable_joins option' do
- expect do
- primary_model.has_many :test_bridges, disable_joins: true
- end.to raise_error(ArgumentError, /Unknown key: :disable_joins/)
- end
- end
-
- context 'when the association is a has_many :through' do
- it 'allows the disable_joins option' do
- primary_model.has_many :test_bridges
- bridge_model.belongs_to :test_secondary
-
- expect do
- primary_model.has_many :test_secondaries, through: :test_bridges, disable_joins: true
- end.not_to raise_error
- end
- end
- end
-
- context 'querying has_one :through when disable_joins is set' do
- before do
- create_tables(<<~SQL)
- CREATE TABLE _test_primary_records (
- id serial NOT NULL PRIMARY KEY);
-
- CREATE TABLE _test_bridge_records (
- id serial NOT NULL PRIMARY KEY,
- primary_record_id int NOT NULL,
- secondary_record_id int NOT NULL);
-
- CREATE TABLE _test_secondary_records (
- id serial NOT NULL PRIMARY KEY);
- SQL
-
- primary_model.has_one :test_bridge, anonymous_class: bridge_model, foreign_key: :primary_record_id
- bridge_model.belongs_to :test_secondary, anonymous_class: secondary_model, foreign_key: :secondary_record_id
- primary_model.has_one :test_secondary,
- through: :test_bridge, anonymous_class: secondary_model, disable_joins: -> { joins_disabled_flag }
-
- primary_record = primary_model.create!
- secondary_record = secondary_model.create!
- bridge_model.create!(primary_record_id: primary_record.id, secondary_record_id: secondary_record.id)
- end
-
- context 'when disable_joins evaluates to true' do
- let(:joins_disabled_flag) { true }
-
- it 'executes separate queries' do
- primary_record = primary_model.first
-
- query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondary }.count
-
- expect(query_count).to eq(2)
- end
- end
-
- context 'when disable_joins evalutes to false' do
- let(:joins_disabled_flag) { false }
-
- it 'executes a single query' do
- primary_record = primary_model.first
-
- query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondary }.count
-
- expect(query_count).to eq(1)
- end
- end
- end
-
- context 'querying has_many :through when disable_joins is set' do
- before do
- create_tables(<<~SQL)
- CREATE TABLE _test_primary_records (
- id serial NOT NULL PRIMARY KEY);
-
- CREATE TABLE _test_bridge_records (
- id serial NOT NULL PRIMARY KEY,
- primary_record_id int NOT NULL);
-
- CREATE TABLE _test_secondary_records (
- id serial NOT NULL PRIMARY KEY,
- bridge_record_id int NOT NULL);
- SQL
-
- primary_model.has_many :test_bridges, anonymous_class: bridge_model, foreign_key: :primary_record_id
- bridge_model.has_many :test_secondaries, anonymous_class: secondary_model, foreign_key: :bridge_record_id
- primary_model.has_many :test_secondaries, through: :test_bridges, anonymous_class: secondary_model,
- disable_joins: -> { disabled_join_flag }
-
- primary_record = primary_model.create!
- bridge_record = bridge_model.create!(primary_record_id: primary_record.id)
- secondary_model.create!(bridge_record_id: bridge_record.id)
- end
-
- context 'when disable_joins evaluates to true' do
- let(:disabled_join_flag) { true }
-
- it 'executes separate queries' do
- primary_record = primary_model.first
-
- query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondaries.first }.count
-
- expect(query_count).to eq(2)
- end
- end
-
- context 'when disable_joins evalutes to false' do
- let(:disabled_join_flag) { false }
-
- it 'executes a single query' do
- primary_record = primary_model.first
-
- query_count = ActiveRecord::QueryRecorder.new { primary_record.test_secondaries.first }.count
-
- expect(query_count).to eq(1)
- end
- end
- end
-
- context 'querying STI relationships' do
- let(:child_bridge_model) do
- Class.new(bridge_model) do
- def self.name
- 'ChildBridge'
- end
- end
- end
-
- let(:child_secondary_model) do
- Class.new(secondary_model) do
- def self.name
- 'ChildSecondary'
- end
- end
- end
-
- before do
- create_tables(<<~SQL)
- CREATE TABLE _test_primary_records (
- id serial NOT NULL PRIMARY KEY);
-
- CREATE TABLE _test_bridge_records (
- id serial NOT NULL PRIMARY KEY,
- primary_record_id int NOT NULL,
- type text);
-
- CREATE TABLE _test_secondary_records (
- id serial NOT NULL PRIMARY KEY,
- bridge_record_id int NOT NULL,
- type text);
- SQL
-
- primary_model.has_many :child_bridges, anonymous_class: child_bridge_model, foreign_key: :primary_record_id
- child_bridge_model.has_one :child_secondary, anonymous_class: child_secondary_model, foreign_key: :bridge_record_id
- primary_model.has_many :child_secondaries, through: :child_bridges, anonymous_class: child_secondary_model, disable_joins: true
-
- primary_record = primary_model.create!
- parent_bridge_record = bridge_model.create!(primary_record_id: primary_record.id)
- child_bridge_record = child_bridge_model.create!(primary_record_id: primary_record.id)
-
- secondary_model.create!(bridge_record_id: child_bridge_record.id)
- child_secondary_model.create!(bridge_record_id: parent_bridge_record.id)
- child_secondary_model.create!(bridge_record_id: child_bridge_record.id)
- end
-
- it 'filters correctly by the STI type across multiple queries' do
- primary_record = primary_model.first
-
- query_recorder = ActiveRecord::QueryRecorder.new do
- expect(primary_record.child_secondaries.count).to eq(1)
- end
-
- expect(query_recorder.count).to eq(2)
- end
- end
-
- context 'querying polymorphic relationships' do
- before do
- create_tables(<<~SQL)
- CREATE TABLE _test_primary_records (
- id serial NOT NULL PRIMARY KEY);
-
- CREATE TABLE _test_bridge_records (
- id serial NOT NULL PRIMARY KEY,
- primaryable_id int NOT NULL,
- primaryable_type text NOT NULL);
-
- CREATE TABLE _test_secondary_records (
- id serial NOT NULL PRIMARY KEY,
- bridgeable_id int NOT NULL,
- bridgeable_type text NOT NULL);
- SQL
-
- primary_model.has_many :test_bridges, anonymous_class: bridge_model, foreign_key: :primaryable_id, as: :primaryable
- bridge_model.has_one :test_secondaries, anonymous_class: secondary_model, foreign_key: :bridgeable_id, as: :bridgeable
- primary_model.has_many :test_secondaries, through: :test_bridges, anonymous_class: secondary_model, disable_joins: true
-
- primary_record = primary_model.create!
- primary_bridge_record = bridge_model.create!(primaryable_id: primary_record.id, primaryable_type: 'TestPrimary')
- nonprimary_bridge_record = bridge_model.create!(primaryable_id: primary_record.id, primaryable_type: 'NonPrimary')
-
- secondary_model.create!(bridgeable_id: primary_bridge_record.id, bridgeable_type: 'TestBridge')
- secondary_model.create!(bridgeable_id: nonprimary_bridge_record.id, bridgeable_type: 'TestBridge')
- secondary_model.create!(bridgeable_id: primary_bridge_record.id, bridgeable_type: 'NonBridge')
- end
-
- it 'filters correctly by the polymorphic type across multiple queries' do
- primary_record = primary_model.first
-
- query_recorder = ActiveRecord::QueryRecorder.new do
- expect(primary_record.test_secondaries.count).to eq(1)
- end
-
- expect(query_recorder.count).to eq(2)
- end
- end
-
- def create_tables(table_sql)
- ApplicationRecord.connection.execute(table_sql)
-
- bridge_model.reset_column_information
- secondary_model.reset_column_information
- end
-end
diff --git a/spec/initializers/100_patch_omniauth_saml_spec.rb b/spec/initializers/100_patch_omniauth_saml_spec.rb
index de556cfa1e5..886f350ca88 100644
--- a/spec/initializers/100_patch_omniauth_saml_spec.rb
+++ b/spec/initializers/100_patch_omniauth_saml_spec.rb
@@ -6,6 +6,15 @@ RSpec.describe 'OmniAuth::Strategies::SAML', type: :strategy do
let(:idp_sso_target_url) { 'https://login.example.com/idp' }
let(:strategy) { [OmniAuth::Strategies::SAML, { idp_sso_target_url: idp_sso_target_url }] }
+ before do
+ mock_session = {}
+
+ allow(mock_session).to receive(:enabled?).and_return(true)
+ allow(mock_session).to receive(:loaded?).and_return(true)
+
+ env('rack.session', mock_session)
+ end
+
describe 'POST /users/auth/saml' do
it 'redirects to the provider login page', :aggregate_failures do
post '/users/auth/saml'
diff --git a/spec/initializers/action_dispatch_journey_router_spec.rb b/spec/initializers/action_dispatch_journey_router_spec.rb
new file mode 100644
index 00000000000..641a8c6d11f
--- /dev/null
+++ b/spec/initializers/action_dispatch_journey_router_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Adds a missing test to provide full coverage for the patch
+RSpec.describe 'ActionDispatch::Journey::Router Patch', feature_category: :database do
+ before do
+ load Rails.root.join('config/initializers/action_dispatch_journey_router.rb')
+ end
+
+ describe '#find_routes' do
+ context 'when a route has additional constrains' do
+ it 'does not raise an error' do
+ stub_const('PagesController', Class.new(ApplicationController))
+
+ set = ActionDispatch::Routing::RouteSet.new
+
+ set.draw do
+ get "*namespace_id/:project_id/bar",
+ to: "pages#show",
+ constraints: {
+ namespace_id: %r{(?!api/)[a-zA-Z0-9_\\]+},
+ project_id: /[a-zA-Z0-9]+/
+ }
+
+ get "/api/foo/bar", to: "pages#index"
+ end
+
+ params = set.recognize_path("/api/foo/bar", method: :get)
+
+ expect(params[:controller]).to eq('pages')
+ expect(params[:action]).to eq('index')
+ end
+ end
+ end
+end
diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb
index b3c4ac5e23b..cd3e3cf0328 100644
--- a/spec/initializers/google_api_client_spec.rb
+++ b/spec/initializers/google_api_client_spec.rb
@@ -8,7 +8,7 @@ require 'google/apis/core/base_service'
RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePath
context('with a successful response') do
let(:client) { Google::Apis::Core::BaseService.new('', '').client }
- let(:command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') }
+ let(:command) { described_class.new(:get, 'https://www.googleapis.com/zoo/animals') }
before do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
diff --git a/spec/initializers/grpc_patch_spec.rb b/spec/initializers/grpc_patch_spec.rb
new file mode 100644
index 00000000000..90d1269e4c1
--- /dev/null
+++ b/spec/initializers/grpc_patch_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'GRPC monkey patch', feature_category: :shared do
+ let(:server) { GRPC::RpcServer.new }
+ let(:stub) do
+ Class.new(Gitaly::CommitService::Service) do
+ def find_all_commits(_request, _call)
+ sleep 1
+
+ nil
+ end
+ end
+ end
+
+ it 'raises DeadlineExceeded on a late server streaming response' do
+ server_port = server.add_http2_port('0.0.0.0:0', :this_port_is_insecure)
+ host = "localhost:#{server_port}"
+ server.handle(stub)
+ thr = Thread.new { server.run }
+
+ stub = Gitaly::CommitService::Stub.new(host, :this_channel_is_insecure)
+ request = Gitaly::FindAllCommitsRequest.new
+ response = stub.find_all_commits(request, deadline: Time.now + 0.1)
+ expect { response.to_a }.to raise_error(GRPC::DeadlineExceeded)
+
+ server.stop
+ thr.join
+ end
+end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 2c396a18361..5c39bee16b2 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe 'create_tokens' do
include StubENV
let(:secrets) { ActiveSupport::OrderedOptions.new }
- let(:hex_key) { /\h{128}/.freeze }
- let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze }
+ let(:hex_key) { /\h{128}/ }
+ let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m }
before do
allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets)
diff --git a/spec/lib/api/entities/bulk_imports/export_batch_status_spec.rb b/spec/lib/api/entities/bulk_imports/export_batch_status_spec.rb
new file mode 100644
index 00000000000..df190a1306f
--- /dev/null
+++ b/spec/lib/api/entities/bulk_imports/export_batch_status_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::BulkImports::ExportBatchStatus, feature_category: :importers do
+ let_it_be(:batch) { create(:bulk_import_export_batch) }
+
+ let(:entity) { described_class.new(batch, request: double) }
+
+ subject { entity.as_json }
+
+ it 'has the correct attributes' do
+ expect(subject).to eq(
+ status: batch.status,
+ batch_number: batch.batch_number,
+ objects_count: batch.objects_count,
+ error: batch.error,
+ updated_at: batch.updated_at
+ )
+ end
+end
diff --git a/spec/lib/api/entities/bulk_imports/export_status_spec.rb b/spec/lib/api/entities/bulk_imports/export_status_spec.rb
index 7d79e372027..b7dbf525a3d 100644
--- a/spec/lib/api/entities/bulk_imports/export_status_spec.rb
+++ b/spec/lib/api/entities/bulk_imports/export_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Entities::BulkImports::ExportStatus do
+RSpec.describe API::Entities::BulkImports::ExportStatus, feature_category: :importers do
let_it_be(:export) { create(:bulk_import_export) }
let(:entity) { described_class.new(export, request: double) }
@@ -10,11 +10,22 @@ RSpec.describe API::Entities::BulkImports::ExportStatus do
subject { entity.as_json }
it 'has the correct attributes' do
- expect(subject).to eq({
+ expect(subject).to eq(
relation: export.relation,
status: export.status,
error: export.error,
- updated_at: export.updated_at
- })
+ updated_at: export.updated_at,
+ batched: export.batched?,
+ batches_count: export.batches_count,
+ total_objects_count: export.total_objects_count
+ )
+ end
+
+ context 'when export is batched' do
+ let_it_be(:export) { create(:bulk_import_export, :batched) }
+
+ it 'exposes batches' do
+ expect(subject).to match(hash_including(batches: []))
+ end
end
end
diff --git a/spec/lib/api/entities/plan_limit_spec.rb b/spec/lib/api/entities/plan_limit_spec.rb
index a2d183fd631..b0ad13995d7 100644
--- a/spec/lib/api/entities/plan_limit_spec.rb
+++ b/spec/lib/api/entities/plan_limit_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe API::Entities::PlanLimit do
:enforcement_limit,
:generic_packages_max_file_size,
:helm_max_file_size,
+ :limits_history,
:maven_max_file_size,
:notification_limit,
:npm_max_file_size,
diff --git a/spec/lib/api/entities/project_spec.rb b/spec/lib/api/entities/project_spec.rb
index 3a5349bb59b..5d18b93228f 100644
--- a/spec/lib/api/entities/project_spec.rb
+++ b/spec/lib/api/entities/project_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ::API::Entities::Project do
let(:options) { { current_user: current_user } }
let(:entity) do
- ::API::Entities::Project.new(project, options)
+ described_class.new(project, options)
end
subject(:json) { entity.as_json }
diff --git a/spec/lib/api/helpers/packages/npm_spec.rb b/spec/lib/api/helpers/packages/npm_spec.rb
index cfb68d2c53e..bd28fdadf02 100644
--- a/spec/lib/api/helpers/packages/npm_spec.rb
+++ b/spec/lib/api/helpers/packages/npm_spec.rb
@@ -3,6 +3,15 @@
require 'spec_helper'
RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registry do # rubocop: disable RSpec/FilePath
+ 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) }
+
+ let(:package_name) { package.name }
+ let(:params) { { id: project.id } }
+ let(:endpoint_scope) { :project }
let(:object) { klass.new(params) }
let(:klass) do
Struct.new(:params) do
@@ -11,12 +20,6 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
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) }
-
before do
allow(object).to receive(:endpoint_scope).and_return(endpoint_scope)
allow(object).to receive(:current_user).and_return(user)
@@ -25,12 +28,7 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
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 } }
- let(:endpoint_scope) { :project }
-
it 'returns a PackageFinder for project scope' do
expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, project: project)
@@ -142,4 +140,10 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
end
end
end
+
+ describe '#enqueue_sync_metadata_cache_worker' do
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ subject { object.enqueue_sync_metadata_cache_worker(project, package_name) }
+ end
+ end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index f8d40d6e181..667ee72f821 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe API::Helpers, feature_category: :shared do
end
it 'handles sticking when a user could be found' do
- allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(user)
+ allow_any_instance_of(described_class).to receive(:initial_current_user).and_return(user)
expect(ApplicationRecord.sticking)
.to receive(:stick_or_unstick_request).with(any_args, :user, 42)
@@ -44,7 +44,7 @@ RSpec.describe API::Helpers, feature_category: :shared do
end
it 'does not handle sticking if no user could be found' do
- allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(nil)
+ allow_any_instance_of(described_class).to receive(:initial_current_user).and_return(nil)
expect(ApplicationRecord.sticking)
.not_to receive(:stick_or_unstick_request)
@@ -55,7 +55,7 @@ RSpec.describe API::Helpers, feature_category: :shared do
end
it 'returns the user if one could be found' do
- allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(user)
+ allow_any_instance_of(described_class).to receive(:initial_current_user).and_return(user)
get 'user'
@@ -609,6 +609,39 @@ RSpec.describe API::Helpers, feature_category: :shared do
end
end
+ describe '#track_event' do
+ let(:user_id) { 345 }
+ let(:namespace_id) { 12 }
+ let(:project_id) { 56 }
+ let(:event_name) { 'i_compliance_dashboard' }
+ let(:unknown_event) { 'unknown' }
+
+ it 'tracks internal event' do
+ expect(Gitlab::InternalEvents).to receive(:track_event).with(
+ event_name,
+ user_id: user_id,
+ namespace_id: namespace_id,
+ project_id: project_id
+ )
+
+ helper.track_event(event_name, user_id: user_id, namespace_id: namespace_id, project_id: project_id)
+ end
+
+ it 'logs an exception for unknown event' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ "Internal Event tracking event failed for event: #{unknown_event}, message: Unknown event: #{unknown_event}"
+ )
+
+ helper.track_event(unknown_event, user_id: user_id, namespace_id: namespace_id, project_id: project_id)
+ end
+
+ it 'does not track event for nil user_id' do
+ expect(Gitlab::InternalEvents).not_to receive(:track_event)
+
+ helper.track_event(unknown_event, user_id: nil, namespace_id: namespace_id, project_id: project_id)
+ end
+ end
+
shared_examples '#order_options_with_tie_breaker' do
subject { Class.new.include(described_class).new.order_options_with_tie_breaker }
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 66ae3658a92..f7597579e7a 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -214,13 +214,7 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
end
describe '#store_deploy_info' do
- let_it_be(:environment) { create(:environment, name: 'DEV', project: project) }
- let_it_be(:deployments) do
- pipelines.map do |p|
- build = create(:ci_build, environment: environment.name, pipeline: p, project: project)
- create(:deployment, deployable: build, environment: environment)
- end
- end
+ let_it_be(:deployments) { create_list(:deployment, 1) }
let(:schema) do
Atlassian::Schemata.deploy_info_payload
@@ -252,18 +246,22 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
subject.send(:store_deploy_info, project: project, deployments: deployments)
end
- it 'only sends information about relevant MRs' do
+ it 'calls the API if issue keys are found' do
expect(subject).to receive(:post).with(
- '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 8) }
+ '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 1) }
).and_call_original
subject.send(:store_deploy_info, project: project, deployments: deployments)
end
- it 'does not call the API if there is nothing to report' do
+ it 'does not call the API if no issue keys are found' do
+ allow_next_instances_of(Atlassian::JiraConnect::Serializers::DeploymentEntity, nil) do |entity|
+ allow(entity).to receive(:issue_keys).and_return([])
+ end
+
expect(subject).not_to receive(:post)
- subject.send(:store_deploy_info, project: project, deployments: deployments.take(1))
+ subject.send(:store_deploy_info, project: project, deployments: deployments)
end
context 'when there are errors' do
diff --git a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
index 523b7ddaa09..57e0b67e9e6 100644
--- a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
@@ -6,18 +6,16 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create_default(:project, :repository) }
let_it_be(:environment) { create(:environment, name: 'prod', project: project) }
- let_it_be_with_reload(:deployment) { create(:deployment, environment: environment) }
+ let_it_be_with_refind(:deployment) { create(:deployment, environment: environment) }
subject { described_class.represent(deployment) }
- context 'when the deployment does not belong to any Jira issue' do
- describe '#issue_keys' do
- it 'is empty' do
- expect(subject.issue_keys).to be_empty
+ describe '#to_json' do
+ context 'when the deployment does not belong to any Jira issue' do
+ before do
+ allow(subject).to receive(:issue_keys).and_return([])
end
- end
- describe '#to_json' do
it 'can encode the object' do
expect(subject.to_json).to be_valid_json
end
@@ -26,9 +24,19 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
expect(subject.to_json).not_to match_schema(Atlassian::Schemata.deployment_info)
end
end
+
+ context 'when the deployment belongs to Jira issue' do
+ before do
+ allow(subject).to receive(:issue_keys).and_return(['JIRA-1'])
+ end
+
+ it 'is valid according to the deployment info schema' do
+ expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info)
+ end
+ end
end
- context 'this is an external deployment' do
+ context 'when deployment is an external deployment' do
before do
deployment.update!(deployable: nil)
end
@@ -36,10 +44,6 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
it 'does not raise errors when serializing' do
expect { subject.to_json }.not_to raise_error
end
-
- it 'returns an empty list of issue keys' do
- expect(subject.issue_keys).to be_empty
- end
end
describe 'environment type' do
@@ -62,27 +66,137 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca
end
end
- context 'when the deployment can be linked to a Jira issue' do
- let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
-
+ describe '#issue_keys' do
+ # For these tests, use a Jira issue key regex that matches a set of commit messages
+ # in the test repo.
+ #
+ # Relevant commits in this test from https://gitlab.com/gitlab-org/gitlab-test/-/commits/master:
+ #
+ # 1) 5f923865dde3436854e9ceb9cdb7815618d4e849 GitLab currently doesn't support patches [...]: add a commit here
+ # 2) 4cd80ccab63c82b4bad16faa5193fbd2aa06df40 add directory structure for tree_helper spec
+ # 3) ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
+ # 4) 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
before do
- subject.deployable.update!(pipeline: pipeline)
+ allow(Gitlab::Regex).to receive(:jira_issue_key_regex).and_return(/add.[a-d]/)
+ end
+
+ let(:expected_issue_keys) { ['add a', 'add d', 'added'] }
+
+ it 'extracts issue keys from the commits' do
+ expect(subject.issue_keys).to contain_exactly(*expected_issue_keys)
+ end
+
+ it 'limits the number of commits scanned' do
+ stub_const("#{described_class}::COMMITS_LIMIT", 10)
+
+ expect(subject.issue_keys).to contain_exactly('add a')
+ end
+
+ context 'when `jira_deployment_issue_keys` flag is disabled' do
+ before do
+ stub_feature_flags(jira_deployment_issue_keys: false)
+ end
+
+ it 'does not extract issue keys from commits' do
+ expect(subject.issue_keys).to be_empty
+ end
+ end
+
+ context 'when deploy happened at an older commit' do
+ before do
+ # SHA is from a commit between 1) and 2) in the commit list above.
+ deployment.update!(sha: 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd')
+ end
+
+ it 'extracts only issue keys from that commit or older' do
+ expect(subject.issue_keys).to contain_exactly('add d', 'added')
+ end
end
- %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) }
+ context 'when the deployment has an associated merge request' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline,
+ merge_request: create(:merge_request,
+ title: 'Title addxa',
+ description: "Description\naddxa\naddya",
+ source_branch: 'feature/addza'
+ )
+ )
+ end
+
+ before do
+ subject.deployable.update!(pipeline: pipeline)
+ end
+
+ it 'includes issue keys extracted from the merge request' do
+ expect(subject.issue_keys).to contain_exactly(
+ *(expected_issue_keys + %w[addxa addya addza])
+ )
+ end
+ end
+
+ context 'when there was a successful deploy to the environment' do
+ let_it_be_with_reload(:last_deploy) do
+ # SHA is from a commit between 2) and 3) in the commit list above.
+ sha = '5937ac0a7beb003549fc5fd26fc247adbce4a52e'
+ create(:deployment, :success, sha: sha, environment: environment, finished_at: 1.hour.ago)
+ end
+
+ shared_examples 'extracts only issue keys from commits made since that deployment' do
+ specify do
+ expect(subject.issue_keys).to contain_exactly('add a', 'add d')
+ end
+ end
+
+ shared_examples 'ignores that deployment' do
+ specify do
+ expect(subject.issue_keys).to contain_exactly(*expected_issue_keys)
+ end
+ end
+
+ it_behaves_like 'extracts only issue keys from commits made since that deployment'
+
+ context 'when the deploy was for a different environment' do
+ before do
+ last_deploy.update!(environment: create(:environment))
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
+
+ context 'when the deploy was for a different branch or tag' do
+ before do
+ last_deploy.update!(ref: 'foo')
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
+
+ context 'when the deploy was not successful' do
+ before do
+ last_deploy.drop!
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
+
+ context 'when the deploy commit cannot be found' do
+ before do
+ last_deploy.update!(sha: 'foo')
+ end
+
+ it_behaves_like 'ignores that deployment'
+ end
- describe '#issue_keys' do
- it 'is not empty' do
- expect(subject.issue_keys).not_to be_empty
- end
+ context 'when there is a more recent deployment' do
+ let_it_be(:more_recent_last_deploy) do
+ # SHA is from a commit between 1) and 2) in the commit list above.
+ sha = 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd'
+ create(:deployment, :success, sha: sha, environment: environment, finished_at: 1.minute.ago)
end
- describe '#to_json' do
- it 'is valid according to the deployment info schema' do
- expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info)
- end
+ it 'extracts only issue keys from commits made since that deployment' do
+ expect(subject.issue_keys).to contain_exactly('add a')
end
end
end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index 2c75377ec42..c8b5a9ffa0b 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -169,7 +169,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning
it 'removes one closing punctuation mark when the punctuation in the link is unbalanced' do
complicated_link = "(#{link}(a'b[c'd]))'"
- expected_complicated_link = %Q{(<a href="#{link}(a'b[c'd]))">#{link}(a'b[c'd]))</a>'}
+ expected_complicated_link = %{(<a href="#{link}(a'b[c'd]))">#{link}(a'b[c'd]))</a>'}
actual = unescape(filter(complicated_link).to_html)
expect(actual).to eq(Rinku.auto_link(complicated_link))
@@ -178,7 +178,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning
it 'does not double-encode HTML entities' do
encoded_link = "#{link}?foo=bar&amp;baz=quux"
- expected_encoded_link = %Q{<a href="#{encoded_link}">#{encoded_link}</a>}
+ expected_encoded_link = %{<a href="#{encoded_link}">#{encoded_link}</a>}
actual = unescape(filter(encoded_link).to_html)
expect(actual).to eq(Rinku.auto_link(encoded_link))
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index de259342998..300b8601dcb 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_plann
it 'skips internal links' do
internal = Gitlab.config.gitlab.url
- exp = act = %Q(<a href="#{internal}/sign_in">Login</a>)
+ exp = act = %(<a href="#{internal}/sign_in">Login</a>)
expect(filter(act).to_html).to eq exp
end
@@ -90,7 +90,7 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_plann
context 'with an impersonated username' do
let(:internal) { Gitlab.config.gitlab.url }
- let(:doc) { filter %Q(<a href="https://#{internal}@example.com" target="_blank">Reverse Tabnabbing</a>) }
+ let(:doc) { filter %(<a href="https://#{internal}@example.com" target="_blank">Reverse Tabnabbing</a>) }
it_behaves_like 'an external link with rel attribute'
end
@@ -112,8 +112,8 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_plann
it 'skips internal links' do
internal_link = Gitlab.config.gitlab.url + "/sign_in"
url = internal_link.gsub(/\Ahttp/, 'HtTp')
- act = %Q(<a href="#{url}">Login</a>)
- exp = %Q(<a href="#{internal_link}">Login</a>)
+ act = %(<a href="#{url}">Login</a>)
+ exp = %(<a href="#{internal_link}">Login</a>)
expect(filter(act).to_html).to eq(exp)
end
@@ -131,7 +131,7 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_plann
context 'links with RTLO character' do
# In rendered text this looks like "http://example.com/evilexe.mp3"
- let(:doc) { filter %Q(<a href="http://example.com/evil%E2%80%AE3pm.exe">http://example.com/evil\u202E3pm.exe</a>) }
+ let(:doc) { filter %(<a href="http://example.com/evil%E2%80%AE3pm.exe">http://example.com/evil\u202E3pm.exe</a>) }
it_behaves_like 'an external link with rel attribute'
@@ -142,7 +142,7 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_plann
end
it 'does not mangle the link text' do
- doc = filter %Q(<a href="http://example.com">One<span>and</span>\u202Eexe.mp3</a>)
+ doc = filter %(<a href="http://example.com">One<span>and</span>\u202Eexe.mp3</a>)
expect(doc.to_html).to include('One<span>and</span>%E2%80%AEexe.mp3</a>')
end
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index 2d496c447e1..6c9e798790f 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe Banzai::Filter::ImageLinkFilter, feature_category: :team_planning
let(:context) { {} }
def image(path, alt: nil, data_src: nil)
- alt_tag = alt ? %Q{alt="#{alt}"} : ""
- data_src_tag = data_src ? %Q{data-src="#{data_src}"} : ""
+ alt_tag = alt ? %{alt="#{alt}"} : ""
+ data_src_tag = data_src ? %{data-src="#{data_src}"} : ""
%(<img src="#{path}" #{alt_tag} #{data_src_tag} />)
end
@@ -22,7 +22,7 @@ RSpec.describe Banzai::Filter::ImageLinkFilter, feature_category: :team_planning
end
it 'does not wrap a duplicate link' do
- doc = filter(%Q(<a href="/whatever">#{image(path)}</a>), context)
+ doc = filter(%(<a href="/whatever">#{image(path)}</a>), context)
expect(doc.to_html).to match %r{^<a href="/whatever"><img[^>]*></a>$}
end
@@ -34,20 +34,28 @@ RSpec.describe Banzai::Filter::ImageLinkFilter, feature_category: :team_planning
end
it 'works with inline images' do
- doc = filter(%Q(<p>test #{image(path)} inline</p>), context)
+ doc = filter(%(<p>test #{image(path)} inline</p>), context)
expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$}
end
it 'keep the data-canonical-src' do
- doc = filter(%q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="http://example.com/test.png" />), context)
+ doc = filter(
+ %q(<img src="http://assets.example.com/6cd/4d7" data-canonical-src="http://example.com/test.png" />),
+ context
+ )
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
expect(doc.at_css('img')['data-canonical-src']).to eq doc.at_css('a')['data-canonical-src']
end
it 'moves the data-diagram* attributes' do
- doc = filter(%q(<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">), context)
+ # rubocop:disable Layout/LineLength
+ doc = filter(
+ %q(<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">),
+ context
+ )
+ # rubocop:enable Layout/LineLength
expect(doc.at_css('a')['data-diagram']).to eq "plantuml"
expect(doc.at_css('a')['data-diagram-src']).to eq "data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw=="
diff --git a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
index 79500f43394..86fb7d3964d 100644
--- a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
@@ -223,7 +223,7 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter, feature
end
context "jira project" do
- let_it_be(:service) { create(:jira_integration, project: project) }
+ let_it_be_with_reload(:service) { create(:jira_integration, project: project) }
let(:reference) { issue.to_reference }
@@ -250,6 +250,36 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter, feature
expect(filter(act).to_html).to eq exp
end
end
+
+ context 'with a custom regex' do
+ before do
+ service.jira_tracker_data.update!(jira_issue_regex: '[JIRA]{2,}-\\d+')
+ end
+
+ context "with right markdown" do
+ let(:issue) { ExternalIssue.new("JIRA-123", project) }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with a single-letter prefix" do
+ let(:issue) { ExternalIssue.new("J-123", project) }
+
+ it "ignores reference" do
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context "with wrong markdown" do
+ let(:issue) { ExternalIssue.new("#123", project) }
+
+ it "ignores reference" do
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
end
context "ewm project" do
diff --git a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
index f8d223c6611..91b051d71ec 100644
--- a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
@@ -344,7 +344,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_categor
end
describe 'referencing a label in a link href' do
- let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} }
+ let(:reference) { %{<a href="#{label.to_reference}">Label</a>} }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
diff --git a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
index ecd5d1368c9..7caa6efff66 100644
--- a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
shared_examples 'referencing a milestone in a link href' do
let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
- let(:link_reference) { %Q{<a href="#{unquoted_reference}">Milestone</a>} }
+ let(:link_reference) { %{<a href="#{unquoted_reference}">Milestone</a>} }
before do
milestone.update!(name: 'gfm')
@@ -169,7 +169,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_cat
shared_examples 'linking to a milestone as the entire link' do
let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
let(:link) { urls.milestone_url(milestone) }
- let(:link_reference) { %Q{<a href="#{link}">#{link}</a>} }
+ let(:link_reference) { %{<a href="#{link}">#{link}</a>} }
it 'replaces the link text with the milestone reference' do
doc = reference_filter("See #{link}")
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 5d56035f6df..6ef03b58f67 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :team_planning
end
it 'escapes the data-original attribute on a reference' do
- markdown = %Q{[">bad things](#{issue.to_reference})}
+ markdown = %{[">bad things](#{issue.to_reference})}
result = described_class.to_html(markdown, project: project)
expect(result).to include(%{data-original='\"&amp;gt;bad things'})
end
diff --git a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb
index 12a6be6bc18..9e79be4333a 100644
--- a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do
context 'when markdown contains labels' do
let(:label) { create(:label, project: project, title: 'backend') }
- let(:markdown) { %Q(~"#{label.name}" ~unknown) }
+ let(:markdown) { %(~"#{label.name}" ~unknown) }
it 'replaces existing label to a link' do
# rubocop:disable Layout/LineLength
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
index 8ff0fa3ae1e..ae01939605e 100644
--- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -64,9 +64,9 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :team_
describe 'backslash escapes are untouched in code blocks, code spans, autolinks, or raw HTML' do
where(:markdown, :expected) do
%q(`` \@\! ``) | %q(<code>\@\!</code>)
- %q( \@\!) | %Q(<code>\\@\\!\n</code>)
- %Q(~~~\n\\@\\!\n~~~) | %Q(<code>\\@\\!\n</code>)
- %q($1+\$2$) | %q(<code data-math-style="inline">1+\\$2</code>)
+ %q( \@\!) | %(<code>\\@\\!\n</code>)
+ %(~~~\n\\@\\!\n~~~) | %(<code>\\@\\!\n</code>)
+ %q($1+\$2$) | %q(<code data-math-style="inline">1+\\$2</code>)
%q(<http://example.com?find=\@>) | %q(<a href="http://example.com?find=%5C@">http://example.com?find=\@</a>)
%q[<a href="/bar\@)">] | %q[<a href="/bar\@)">]
end
@@ -77,15 +77,15 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :team_
end
describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
- let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
+ let(:markdown) { %(``` foo\\@bar\nfoo\n```) }
it 'renders correct html' do
- correct_html_included(markdown, %Q(<pre lang="foo@bar"><code>foo\n</code></pre>))
+ correct_html_included(markdown, %(<pre lang="foo@bar"><code>foo\n</code></pre>))
end
where(:markdown, :expected) do
- %q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
- %Q![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
+ %q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
+ %![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
end
with_them do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 2efdb928b6f..072df6a23aa 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Banzai::ReferenceParser::IssueParser, feature_category: :team_pla
end
def issue_link(issue)
- Nokogiri::HTML.fragment(%Q{<a data-issue="#{issue.id}"></a>}).children[0]
+ Nokogiri::HTML.fragment(%{<a data-issue="#{issue.id}"></a>}).children[0]
end
before do
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index eead5019217..bab535b67bf 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser, feature_category: :c
end
def merge_request_link(merge_request)
- Nokogiri::HTML.fragment(%Q{<a data-project="#{merge_request.project_id}" data-merge-request="#{merge_request.id}"></a>}).children[0]
+ Nokogiri::HTML.fragment(%{<a data-project="#{merge_request.project_id}" data-merge-request="#{merge_request.id}"></a>}).children[0]
end
before do
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index cb2da24b712..a1425169dee 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -338,7 +338,7 @@ RSpec.describe ContainerRegistry::Client do
with_them do
before do
- allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(is_on_dot_com)
stub_registry_tags_support(registry_tags_support_enabled)
stub_application_setting(container_registry_features: container_registry_features)
end
@@ -403,7 +403,7 @@ RSpec.describe ContainerRegistry::Client do
with_them do
before do
- allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(is_on_dot_com)
stub_container_registry_config(enabled: registry_enabled, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
stub_registry_tags_support(registry_tags_support_enabled)
stub_application_setting(container_registry_features: container_registry_features)
diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb
index c70dd265073..ebc69201513 100644
--- a/spec/lib/container_registry/gitlab_api_client_spec.rb
+++ b/spec/lib/container_registry/gitlab_api_client_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
with_them do
before do
- allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(is_on_dot_com)
stub_registry_gitlab_api_support(registry_gitlab_api_enabled)
stub_application_setting(container_registry_features: container_registry_features)
end
@@ -46,7 +46,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
context 'with 401 response' do
before do
- allow(::Gitlab).to receive(:com?).and_return(false)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(false)
stub_application_setting(container_registry_features: [])
stub_request(:get, "#{registry_api_url}/gitlab/v1/")
.to_return(status: 401, body: '')
@@ -428,7 +428,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
with_them do
before do
- allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(is_on_dot_com)
stub_container_registry_config(enabled: true, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
stub_registry_gitlab_api_support(registry_gitlab_api_enabled)
stub_application_setting(container_registry_features: container_registry_features)
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 0c5d587d8e8..ad73665326a 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
-RSpec.describe ExpandVariables do
+RSpec.describe ExpandVariables, feature_category: :secrets_management do
shared_examples 'common variable expansion' do |expander|
using RSpec::Parameterized::TableSyntax
@@ -35,7 +35,14 @@ RSpec.describe ExpandVariables do
{ key: 'variable', value: 'value' }
]
},
- "simple expansions": {
+ "expansion using %": {
+ value: 'key%variable%',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ "multiple simple expansions": {
value: 'key$variable$variable2',
result: 'keyvalueresult',
variables: [
@@ -43,7 +50,7 @@ RSpec.describe ExpandVariables do
{ key: 'variable2', value: 'result' }
]
},
- "complex expansions": {
+ "multiple complex expansions": {
value: 'key${variable}${variable2}',
result: 'keyvalueresult',
variables: [
@@ -51,6 +58,15 @@ RSpec.describe ExpandVariables do
{ key: 'variable2', value: 'result' }
]
},
+ "nested expansion is not expanded": {
+ value: 'key$variable$variable2',
+ result: 'keyvalue$variable3',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: '$variable3' },
+ { key: 'variable3', value: 'result' }
+ ]
+ },
"out-of-order expansion": {
value: 'key$variable2$variable',
result: 'keyresultvalue',
@@ -99,10 +115,86 @@ RSpec.describe ExpandVariables do
end
end
+ shared_examples 'file variable expansion with expand_file_refs true' do |expander|
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "simple with a file variable": {
+ value: 'key$variable',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value', file: true }
+ ]
+ },
+ "complex expansion with a file variable": {
+ value: 'key${variable}',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value', file: true }
+ ]
+ },
+ "expansion using % with a file variable": {
+ value: 'key%variable%',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value', file: true }
+ ]
+ }
+ }
+ end
+
+ with_them do
+ subject { expander.call(value, variables, expand_file_refs: true) }
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ shared_examples 'file variable expansion with expand_file_refs false' do |expander|
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "simple with a file variable": {
+ value: 'key$variable',
+ result: 'key$variable',
+ variables: [
+ { key: 'variable', value: 'value', file: true }
+ ]
+ },
+ "complex expansion with a file variable": {
+ value: 'key${variable}',
+ result: 'key${variable}',
+ variables: [
+ { key: 'variable', value: 'value', file: true }
+ ]
+ },
+ "expansion using % with a file variable": {
+ value: 'key%variable%',
+ result: 'key%variable%',
+ variables: [
+ { key: 'variable', value: 'value', file: true }
+ ]
+ }
+ }
+ end
+
+ with_them do
+ subject { expander.call(value, variables, expand_file_refs: false) }
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
describe '#expand' do
context 'table tests' do
it_behaves_like 'common variable expansion', described_class.method(:expand)
+ it_behaves_like 'file variable expansion with expand_file_refs true', described_class.method(:expand)
+
+ it_behaves_like 'file variable expansion with expand_file_refs false', described_class.method(:expand)
+
context 'with missing variables' do
using RSpec::Parameterized::TableSyntax
@@ -169,6 +261,10 @@ RSpec.describe ExpandVariables do
context 'table tests' do
it_behaves_like 'common variable expansion', described_class.method(:expand_existing)
+ it_behaves_like 'file variable expansion with expand_file_refs true', described_class.method(:expand_existing)
+
+ it_behaves_like 'file variable expansion with expand_file_refs false', described_class.method(:expand_existing)
+
context 'with missing variables' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/extracts_ref/requested_ref_spec.rb b/spec/lib/extracts_ref/requested_ref_spec.rb
new file mode 100644
index 00000000000..80d3575b360
--- /dev/null
+++ b/spec/lib/extracts_ref/requested_ref_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExtractsRef::RequestedRef, feature_category: :source_code_management do
+ describe '#find' do
+ subject { described_class.new(project.repository, ref_type: ref_type, ref: ref).find }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let(:ref_type) { nil }
+
+ # Create branches and tags consistently with the same shas to make comparison easier to follow
+ let(:tag_sha) { RepoHelpers.sample_commit.id }
+ let(:branch_sha) { RepoHelpers.another_sample_commit.id }
+
+ shared_context 'when a branch exists' do
+ before do
+ project.repository.create_branch(branch_name, branch_sha)
+ end
+
+ after do
+ project.repository.rm_branch(project.owner, branch_name)
+ end
+ end
+
+ shared_context 'when a tag exists' do
+ before do
+ project.repository.add_tag(project.owner, tag_name, tag_sha)
+ end
+
+ after do
+ project.repository.rm_tag(project.owner, tag_name)
+ end
+ end
+
+ shared_examples 'RequestedRef when ref_type is specified' do |branch_sha, tag_sha|
+ context 'when ref_type is heads' do
+ let(:ref_type) { 'heads' }
+
+ it 'returns the branch commit' do
+ expect(subject[:ref_type]).to eq('heads')
+ expect(subject[:commit].id).to eq(branch_sha)
+ end
+ end
+
+ context 'when ref_type is tags' do
+ let(:ref_type) { 'tags' }
+
+ it 'returns the tag commit' do
+ expect(subject[:ref_type]).to eq('tags')
+ expect(subject[:commit].id).to eq(tag_sha)
+ end
+ end
+ end
+
+ context 'when the ref is the sha for a commit' do
+ let(:ref) { branch_sha }
+
+ context 'and a tag and branch with that sha as a name' do
+ include_context 'when a branch exists' do
+ let(:branch_name) { ref }
+ end
+
+ include_context 'when a tag exists' do
+ let(:tag_name) { ref }
+ end
+
+ it_behaves_like 'RequestedRef when ref_type is specified',
+ RepoHelpers.another_sample_commit.id,
+ RepoHelpers.sample_commit.id
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit].id).to eq(ref)
+ end
+ end
+ end
+
+ context 'when ref is for a tag' do
+ include_context 'when a tag exists' do
+ let(:tag_name) { SecureRandom.uuid }
+ end
+
+ let(:ref) { tag_name }
+
+ it 'returns the tag commit' do
+ expect(subject[:ref_type]).to eq('tags')
+ expect(subject[:commit].id).to eq(tag_sha)
+ end
+
+ context 'and there is a branch with the same name' do
+ include_context 'when a branch exists' do
+ let(:branch_name) { ref }
+ end
+
+ it_behaves_like 'RequestedRef when ref_type is specified',
+ RepoHelpers.another_sample_commit.id,
+ RepoHelpers.sample_commit.id
+
+ it 'returns the tag commit' do
+ expect(subject[:ref_type]).to eq('tags')
+ expect(subject[:commit].id).to eq(tag_sha)
+ expect(subject[:ambiguous]).to be_truthy
+ end
+ end
+ end
+
+ context 'when ref is only for a branch' do
+ let(:ref) { SecureRandom.uuid }
+
+ include_context 'when a branch exists' do
+ let(:branch_name) { ref }
+ end
+
+ it 'returns the branch commit' do
+ expect(subject[:ref_type]).to eq('heads')
+ expect(subject[:commit].id).to eq(branch_sha)
+ end
+ end
+
+ context 'when ref is an abbreviated commit sha' do
+ let(:ref) { branch_sha.first(8) }
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit].id).to eq(branch_sha)
+ end
+ end
+
+ context 'when ref does not exist' do
+ let(:ref) { SecureRandom.uuid }
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit]).to be_nil
+ end
+ end
+
+ context 'when ref is symbolic' do
+ let(:ref) { "heads/#{branch_name}" }
+
+ include_context 'when a branch exists' do
+ let(:branch_name) { SecureRandom.uuid }
+ end
+
+ it 'returns the commit' do
+ expect(subject[:ref_type]).to be_nil
+ expect(subject[:commit].id).to eq(branch_sha)
+ expect(subject[:ambiguous]).to be_truthy
+ end
+ end
+ end
+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
index d533bcf0039..d60d0c3c853 100644
--- 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
@@ -20,15 +20,17 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
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]
+ shared_examples "generates files common to both types of migrations" do |migration_job_file, migration_file,
+ migration_spec_file, migration_dictionary_file|
+ let(:expected_migration_job_file) { load_expected_file(migration_job_file) }
+ let(:expected_migration_file) { load_expected_file(migration_file) }
+ let(:expected_migration_spec_file) { load_expected_file(migration_spec_file) }
+ let(:expected_migration_dictionary) { load_expected_file(migration_dictionary_file) }
+
+ it 'generates expected common files' do
+ 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_migration('db/post_migrate/queue_my_batched_migration.rb') do |migration_file|
expect(migration_file).to eq(expected_migration_file)
@@ -38,15 +40,6 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
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}/)
@@ -54,23 +47,50 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
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')
+ context 'when generating EE-only batched background migration' do
+ before do
+ run_generator %w[my_batched_migration --table_name=projects --column_name=id --feature_category=database
+ --ee-only]
+ end
+
+ let(:expected_ee_migration_job_file) { load_expected_file('ee_my_batched_migration.txt') }
+ let(:expected_migration_job_spec_file) { load_expected_file('my_batched_migration_spec_matcher.txt') }
+
+ include_examples "generates files common to both types of migrations",
+ 'foss_my_batched_migration.txt',
+ 'queue_my_batched_migration.txt',
+ 'queue_my_batched_migration_spec.txt',
+ 'my_batched_migration_dictionary_matcher.txt'
+
+ it 'generates expected files' do
+ assert_file('ee/lib/ee/gitlab/background_migration/my_batched_migration.rb') do |migration_job_file|
+ expect(migration_job_file).to eq(expected_ee_migration_job_file)
+ end
+
+ assert_file('ee/spec/lib/ee/gitlab/background_migration/my_batched_migration_spec.rb') do |migration_job_spec_file| # rubocop:disable Layout/LineLength
+ expect(migration_job_spec_file).to match(/#{expected_migration_job_spec_file}/)
+ end
end
+ 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')
+ context 'when generating FOSS batched background migration' do
+ before do
+ run_generator %w[my_batched_migration --table_name=projects --column_name=id --feature_category=database]
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')
+ let(:expected_migration_job_spec_file) { load_expected_file('my_batched_migration_spec_matcher.txt') }
+
+ include_examples "generates files common to both types of migrations",
+ 'my_batched_migration.txt',
+ 'queue_my_batched_migration.txt',
+ 'queue_my_batched_migration_spec.txt',
+ 'my_batched_migration_dictionary_matcher.txt'
+
+ it 'generates expected files' do
+ 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
end
end
diff --git a/spec/lib/generators/batched_background_migration/expected_files/ee_my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/ee_my_batched_migration.txt
new file mode 100644
index 00000000000..004ae46ca5f
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/ee_my_batched_migration.txt
@@ -0,0 +1,29 @@
+# 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 EE
+ module Gitlab
+ module BackgroundMigration
+ module MyBatchedMigration
+ extend ActiveSupport::Concern
+ extend ::Gitlab::Utils::Override
+
+ prepended do
+ # operation_name :my_operation
+ # scope_to ->(relation) { relation.where(column: "value") }
+ end
+
+ override :perform
+ def perform
+ each_sub_batch do |sub_batch|
+ # Your action on each sub_batch
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/generators/batched_background_migration/expected_files/foss_my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/foss_my_batched_migration.txt
new file mode 100644
index 00000000000..7c2e1f4fc04
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/foss_my_batched_migration.txt
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # TODO Add a top-level documentation comment for the class
+ class MyBatchedMigration < BatchedMigrationJob
+ feature_category :database
+
+ def perform; end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::MyBatchedMigration.prepend_mod
diff --git a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
index 517ba4d7699..d9acd59aa71 100644
--- a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
+++ b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
let(:section) { "analytics" }
let(:mr) { "https://gitlab.com/some-group/some-project/-/merge_requests/123" }
let(:event) { "view_analytics_dashboard" }
- let(:unique_on) { "user_id" }
+ let(:unique) { "user_id" }
let(:time_frames) { %w[7d] }
let(:include_default_identifiers) { 'yes' }
let(:options) do
@@ -27,11 +27,11 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
stage: stage,
section: section,
event: event,
- unique_on: unique_on
+ unique: unique
}.stringify_keys
end
- let(:key_path_7d) { "count_distinct_#{unique_on}_from_#{event}_7d" }
+ let(:key_path_7d) { "count_distinct_#{unique}_from_#{event}_7d" }
let(:metric_definition_path_7d) { Dir.glob(File.join(temp_dir, "metrics/counts_7d/#{key_path_7d}.yml")).first }
let(:metric_definition_7d) do
{
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
"milestone" => "13.9",
"introduced_by_url" => mr,
"time_frame" => "7d",
- "data_source" => "redis_hll",
+ "data_source" => "internal_events",
"data_category" => "optional",
"instrumentation_class" => "RedisHLLMetric",
"distribution" => %w[ce ee],
@@ -195,6 +195,14 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
end
end
+ context 'with unique value passed with a dot' do
+ it 'creates a metric definition file using the template' do
+ described_class.new([], options.merge(unique: 'user.id')).invoke_all
+
+ expect(YAML.safe_load(File.read(metric_definition_path_7d))).to eq(metric_definition_7d)
+ end
+ end
+
context 'without at least one tier available' do
it 'raises error' do
expect { described_class.new([], options.merge(tiers: [])).invoke_all }
@@ -211,8 +219,8 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
context 'without obligatory parameter' do
it 'raises error', :aggregate_failures do
- %w[unique_on event mr section stage group].each do |option|
- expect { described_class.new([], options.without(option)).invoke_all }
+ %w[unique event mr section stage group].each do |option|
+ expect { described_class.new([], options.without(option)).invoke_all }
.to raise_error(RuntimeError)
end
end
@@ -241,7 +249,7 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
context 'for multiple time frames' do
let(:time_frames) { %w[7d 28d] }
- let(:key_path_28d) { "count_distinct_#{unique_on}_from_#{event}_28d" }
+ let(:key_path_28d) { "count_distinct_#{unique}_from_#{event}_28d" }
let(:metric_definition_path_28d) { Dir.glob(File.join(temp_dir, "metrics/counts_28d/#{key_path_28d}.yml")).first }
let(:metric_definition_28d) do
metric_definition_7d.merge(
@@ -260,7 +268,7 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
context 'with default time frames' do
let(:time_frames) { nil }
- let(:key_path_28d) { "count_distinct_#{unique_on}_from_#{event}_28d" }
+ let(:key_path_28d) { "count_distinct_#{unique}_from_#{event}_28d" }
let(:metric_definition_path_28d) { Dir.glob(File.join(temp_dir, "metrics/counts_28d/#{key_path_28d}.yml")).first }
let(:metric_definition_28d) do
metric_definition_7d.merge(
diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb
index 5ab610dfc8f..e54ff8807b5 100644
--- a/spec/lib/gitlab/access/branch_protection_spec.rb
+++ b/spec/lib/gitlab/access/branch_protection_spec.rb
@@ -82,4 +82,62 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
end
end
+
+ describe '#to_hash' do
+ context 'for allow_force_push' do
+ subject { described_class.new(level).to_hash[:allow_force_push] }
+
+ where(:level, :result) do
+ Gitlab::Access::PROTECTION_NONE | true
+ Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false
+ Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true
+ Gitlab::Access::PROTECTION_FULL | false
+ Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | true
+ end
+
+ with_them { it { is_expected.to eq(result) } }
+ end
+
+ context 'for allowed_to_push' do
+ subject { described_class.new(level).to_hash[:allowed_to_push] }
+
+ where(:level, :result) do
+ Gitlab::Access::PROTECTION_NONE | [{ 'access_level' => Gitlab::Access::DEVELOPER }]
+ Gitlab::Access::PROTECTION_DEV_CAN_PUSH | [{ 'access_level' => Gitlab::Access::DEVELOPER }]
+ Gitlab::Access::PROTECTION_DEV_CAN_MERGE | [{ 'access_level' => Gitlab::Access::MAINTAINER }]
+ Gitlab::Access::PROTECTION_FULL | [{ 'access_level' => Gitlab::Access::MAINTAINER }]
+ Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | [{ 'access_level' => Gitlab::Access::MAINTAINER }]
+ end
+
+ with_them { it { is_expected.to eq(result) } }
+ end
+
+ context 'for allowed_to_merge' do
+ subject { described_class.new(level).to_hash[:allowed_to_merge] }
+
+ where(:level, :result) do
+ Gitlab::Access::PROTECTION_NONE | [{ 'access_level' => Gitlab::Access::DEVELOPER }]
+ Gitlab::Access::PROTECTION_DEV_CAN_PUSH | [{ 'access_level' => Gitlab::Access::DEVELOPER }]
+ Gitlab::Access::PROTECTION_DEV_CAN_MERGE | [{ 'access_level' => Gitlab::Access::DEVELOPER }]
+ Gitlab::Access::PROTECTION_FULL | [{ 'access_level' => Gitlab::Access::MAINTAINER }]
+ Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | [{ 'access_level' => Gitlab::Access::DEVELOPER }]
+ end
+
+ with_them { it { is_expected.to eq(result) } }
+ end
+
+ context 'for developer_can_initial_push' do
+ subject { described_class.new(level).to_hash[:developer_can_initial_push] }
+
+ where(:level, :result) do
+ Gitlab::Access::PROTECTION_NONE | nil
+ Gitlab::Access::PROTECTION_DEV_CAN_PUSH | nil
+ Gitlab::Access::PROTECTION_DEV_CAN_MERGE | nil
+ Gitlab::Access::PROTECTION_FULL | nil
+ Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH | true
+ end
+
+ with_them { it { is_expected.to eq(result) } }
+ end
+ end
end
diff --git a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb
index fa8afd47c53..d7184c89933 100644
--- a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb
@@ -150,20 +150,4 @@ RSpec.describe Gitlab::AlertManagement::Payload::ManagedPrometheus do
it { is_expected.to eq(environment) }
end
end
-
- describe '#metrics_dashboard_url' do
- subject { parsed_payload.metrics_dashboard_url }
-
- context 'without alert' do
- it { is_expected.to be_nil }
- end
-
- context 'with gitlab alert' do
- include_context 'gitlab-managed prometheus alert attributes' do
- let(:raw_payload) { payload }
- end
-
- it { is_expected.to eq(dashboard_url_for_alert) }
- end
- end
end
diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
index 8ead292c27a..92836915f7b 100644
--- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
@@ -178,34 +178,6 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
end
end
- describe '#metrics_dashboard_url' do
- include_context 'self-managed prometheus alert attributes' do
- let(:raw_payload) { payload }
- end
-
- subject { parsed_payload.metrics_dashboard_url }
-
- it { is_expected.to eq(dashboard_url_for_alert) }
-
- context 'without environment' do
- let(:raw_payload) { payload.except('labels') }
-
- it { is_expected.to be_nil }
- end
-
- context 'without full query' do
- let(:raw_payload) { payload.except('generatorURL') }
-
- it { is_expected.to be_nil }
- end
-
- context 'without title' do
- let(:raw_payload) { payload.except('annotations') }
-
- it { is_expected.to be_nil }
- end
- end
-
describe '#has_required_attributes?' do
let(:starts_at) { Time.current.change(usec: 0).utc }
let(:raw_payload) { { 'annotations' => { 'title' => 'title' }, 'startsAt' => starts_at.rfc3339 } }
diff --git a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
index de1b3e2af71..26495ab4bc6 100644
--- a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
+++ b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Asciidoc::Html5Converter do
it 'appends user-content- prefix on ref (anchor)' do
doc = Asciidoctor::Document.new('')
anchor = Asciidoctor::Inline.new(doc, :anchor, '', type: :ref, id: 'cross-references')
- converter = Gitlab::Asciidoc::Html5Converter.new('gitlab_html5')
+ converter = described_class.new('gitlab_html5')
html = converter.convert_inline_anchor(anchor)
expect(html).to eq('<a id="user-content-cross-references"></a>')
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 4498e369695..1a1e165c50a 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
set_header('HTTP_ACCEPT', 'application/atom+xml')
end
- context 'when feed_token param is provided' do
+ context 'when old format feed_token param is provided' do
it 'returns user if valid feed_token' do
set_param(:feed_token, user.feed_token)
@@ -206,7 +206,44 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
end
end
- context 'when rss_token param is provided' do
+ context 'when path-dependent format feed_token param is provided' do
+ let_it_be(:feed_user, freeze: true) { create(:user, feed_token: 'KNOWN VALUE').tap(&:feed_token) }
+ # The middle part is the output of OpenSSL::HMAC.hexdigest("SHA256", 'KNOWN VALUE', 'url.atom')
+ let(:feed_token) { "glft-a8cc74ccb0de004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693-#{feed_user.id}" }
+
+ it 'returns user if valid feed_token' do
+ set_param(:feed_token, feed_token)
+
+ expect(find_user_from_feed_token(:rss)).to eq feed_user
+ end
+
+ it 'returns nil if valid feed_token and disabled' do
+ allow(Gitlab::CurrentSettings).to receive_messages(disable_feed_token: true)
+ set_param(:feed_token, feed_token)
+
+ expect(find_user_from_feed_token(:rss)).to be_nil
+ end
+
+ it 'returns exception if token has same HMAC but different user ID' do
+ set_param(:feed_token, "glft-a8cc74ccb0de004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693-#{user.id}")
+
+ expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+
+ it 'returns exception if token has wrong HMAC but same user ID' do
+ set_param(:feed_token, "glft-aaaaaaaaaade004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693-#{feed_user.id}")
+
+ expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+
+ it 'returns exception if user does not exist' do
+ set_param(:feed_token, "glft-a8cc74ccb0de004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693-#{non_existing_record_id}")
+
+ expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ context 'when old format rss_token param is provided' do
it 'returns user if valid rss_token' do
set_param(:rss_token, user.feed_token)
@@ -468,6 +505,40 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
end
end
end
+
+ context 'automatic reuse detection' do
+ let(:token_3) { create(:personal_access_token, :revoked) }
+ let(:token_2) { create(:personal_access_token, :revoked, previous_personal_access_token_id: token_3.id) }
+ let(:token_1) { create(:personal_access_token, previous_personal_access_token_id: token_2.id) }
+
+ context 'when a revoked token is used' do
+ before do
+ set_bearer_token(token_3.token)
+ end
+
+ it 'revokes the latest rotated token' do
+ expect(token_1).not_to be_revoked
+
+ expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
+
+ expect(token_1.reload).to be_revoked
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(pat_reuse_detection: false)
+ end
+
+ it 'does not revoke the latest rotated token' do
+ expect(token_1).not_to be_revoked
+
+ expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
+
+ expect(token_1.reload).not_to be_revoked
+ end
+ end
+ end
+ end
end
describe '#find_user_from_web_access_token' do
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b864dba58de..c4fa8513618 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
- expect(subject::API_SCOPES).to match_array %i[api read_user read_api]
+ expect(subject::API_SCOPES).to match_array %i[api read_user read_api create_runner]
end
it 'ADMIN_SCOPES contains all scopes for ADMIN access' do
@@ -40,29 +40,29 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'contains all non-default scopes' do
- expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode read_observability write_observability]
+ expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode read_observability write_observability create_runner]
end
it 'contains for non-admin user all non-default scopes without ADMIN access and without observability scopes' do
user = build_stubbed(:user, admin: false)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry create_runner]
end
it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes' do
user = build_stubbed(:user, admin: true)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner]
end
it 'contains for project all resource bot scopes without observability scopes' do
- expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry]
+ expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner]
end
it 'contains for group all resource bot scopes' do
group = build_stubbed(:group)
- expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability]
+ expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner]
end
it 'contains for unsupported type no scopes' do
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'optional_scopes contains all non-default scopes' do
- expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode openid profile email read_observability write_observability]
+ expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode openid profile email read_observability write_observability create_runner]
end
context 'with observability_group_tab feature flag' do
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
it 'contains for group all resource bot scopes without observability scopes' do
group = build_stubbed(:group)
- expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry]
+ expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner]
end
end
@@ -94,23 +94,23 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'contains for other group all resource bot scopes including observability scopes' do
- expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability]
+ expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner]
end
it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes' do
user = build_stubbed(:user, admin: true)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner]
end
it 'contains for project all resource bot scopes without observability scopes' do
- expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry]
+ expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner]
end
it 'contains for other group all resource bot scopes without observability scopes' do
other_group = build_stubbed(:group)
- expect(subject.available_scopes_for(other_group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry]
+ expect(subject.available_scopes_for(other_group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner]
end
end
end
@@ -242,6 +242,20 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
end
+ it 'recognises project level security_policy_bot access token' do
+ build.update!(user: create(:user, :security_policy_bot))
+ project.add_guest(build.user)
+
+ expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
+ end
+
+ it 'recognises group level security_policy_bot access token' do
+ build.update!(user: create(:user, :security_policy_bot))
+ group.add_guest(build.user)
+
+ expect(subject).to have_attributes(actor: build.user, project: build.project, type: :build, authentication_abilities: described_class.build_authentication_abilities)
+ end
+
it 'fails with blocked user token' do
build.update!(user: create(:user, :blocked))
@@ -351,6 +365,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
'read_api' | described_class.read_only_authentication_abilities
'read_repository' | [:download_code]
'write_repository' | [:download_code, :push_code]
+ 'create_runner' | [:create_instance_runner, :create_runner]
'read_user' | []
'sudo' | []
'openid' | []
@@ -412,6 +427,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect_results_with_abilities(personal_access_token, [:download_code, :push_code])
end
+ it 'succeeds for personal access tokens with the `create_runner` scope' do
+ personal_access_token = create(:personal_access_token, scopes: ['create_runner'])
+
+ expect_results_with_abilities(personal_access_token, [:create_instance_runner, :create_runner])
+ end
+
context 'when registry is enabled' do
before do
stub_container_registry_config(enabled: true)
diff --git a/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb
new file mode 100644
index 00000000000..8f7d5f25a80
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillMissingCiCdSettings, schema: 20230628023103, feature_category: :source_code_management do # rubocop:disable Layout/LineLength
+ let(:projects_table) { table(:projects) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:ci_cd_settings_table) { table(:project_ci_cd_settings) }
+
+ let(:namespace_1) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-1') }
+
+ let(:project_namespace_2) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') }
+ let(:project_namespace_3) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-3', type: 'Project') }
+ let(:project_namespace_4) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') }
+ let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') }
+ let!(:project_1) do
+ projects_table
+ .create!(
+ name: 'project1',
+ path: 'path1',
+ namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_2.id,
+ visibility_level: 0
+ )
+ end
+
+ let!(:project_2) do
+ projects_table
+ .create!(
+ name: 'project2',
+ path: 'path2',
+ namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_3.id,
+ visibility_level: 0
+ )
+ end
+
+ let!(:project_3) do
+ projects_table
+ .create!(
+ name: 'project3',
+ path: 'path3',
+ namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_4.id,
+ visibility_level: 0
+ )
+ end
+
+ let!(:ci_cd_settings_3) do
+ ci_cd_settings_table.create!(project_id: project_3.id)
+ end
+
+ let!(:project_4) do
+ projects_table
+ .create!(
+ name: 'project4',
+ path: 'path4',
+ namespace_id: namespace_1.id,
+ project_namespace_id: project_namespace_5.id,
+ visibility_level: 0
+ )
+ end
+
+ subject(:perform_migration) do
+ described_class.new(start_id: projects_table.minimum(:id),
+ end_id: projects_table.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: projects_table.connection)
+ .perform
+ end
+
+ it 'creates ci_cd_settings for projects without ci_cd_settings' do
+ expect { subject }.to change { ci_cd_settings_table.count }.from(1).to(4)
+ end
+
+ it 'creates ci_cd_settings with default values' do
+ ci_cd_settings_table.where.not(project_id: ci_cd_settings_3.project_id).each do |ci_cd_setting|
+ expect(ci_cd_setting.attributes.except('id', 'project_id')).to eq({
+ "group_runners_enabled" => true,
+ "merge_pipelines_enabled" => nil,
+ "default_git_depth" => 20,
+ "forward_deployment_enabled" => true,
+ "merge_trains_enabled" => false,
+ "auto_rollback_enabled" => false,
+ "keep_latest_artifact" => false,
+ "restrict_user_defined_variables" => false,
+ "job_token_scope_enabled" => false,
+ "runner_token_expiration_interval" => nil,
+ "separated_caches" => true,
+ "allow_fork_pipelines_to_run_in_parent_project" => true,
+ "inbound_job_token_scope_enabled" => true
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb b/spec/lib/gitlab/background_migration/backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb
new file mode 100644
index 00000000000..699fa39c309
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillUuidConversionColumnInVulnerabilityOccurrences, schema: 20230629095819, feature_category: :vulnerability_management do # rubocop:disable Layout/LineLength
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+ let(:members) { table(:members) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let(:vulnerability_scanners) { table(:vulnerability_scanners) }
+ let(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let!(:user) { create_user(email: "test1@example.com", username: "test1") }
+ let!(:namespace) { namespaces.create!(name: "test-1", path: "test-1", owner_id: user.id) }
+ let!(:project) do
+ projects.create!(
+ id: 9999, namespace_id: namespace.id,
+ project_namespace_id: namespace.id,
+ creator_id: user.id
+ )
+ end
+
+ let!(:membership) do
+ members.create!(access_level: 50, source_id: project.id, source_type: "Project", user_id: user.id, state: 0,
+ notification_level: 3, type: "ProjectMember", member_namespace_id: namespace.id)
+ end
+
+ let(:scanner) { create_scanner(project) }
+ let(:null_uuid) { '00000000-0000-0000-0000-000000000000' }
+
+ let(:migration_attrs) do
+ {
+ start_id: finding_with_no_converted_uuid_1.id,
+ end_id: finding_with_converted_uuid.id,
+ batch_table: :vulnerability_occurrences,
+ batch_column: :id,
+ sub_batch_size: 100,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ }
+ end
+
+ describe "#perform" do
+ subject(:background_migration) { described_class.new(**migration_attrs).perform }
+
+ before do
+ # We have to disable it in tests because it's triggered BEFORE INSERT OR UPDATE ON vulnerability_occurrences
+ # so it's hard to create or update the uuid_convert_string_to_uuid with null UUID value
+
+ # In reality the UPDATE query in the batched background migration could be SET id = id which would
+ # trigger UUID update trigger but we can't exactly do that and expect readable tests
+ ApplicationRecord.connection.execute("ALTER TABLE vulnerability_occurrences DISABLE TRIGGER trigger_1a857e8db6cd")
+ end
+
+ let(:finding_with_no_converted_uuid_1) do
+ create_finding(project, scanner, uuid_convert_string_to_uuid: null_uuid)
+ end
+
+ let(:finding_with_converted_uuid) do
+ uuid = SecureRandom.uuid
+ create_finding(project, scanner, uuid: uuid, uuid_convert_string_to_uuid: uuid)
+ end
+
+ after do
+ ApplicationRecord.connection.execute("ALTER TABLE vulnerability_occurrences ENABLE TRIGGER trigger_1a857e8db6cd")
+ end
+
+ it "backfills the uuid_convert_string_to_uuid column" do
+ expect { background_migration }.to change { finding_with_no_converted_uuid_1.reload.uuid_convert_string_to_uuid }
+ .from(null_uuid).to(finding_with_no_converted_uuid_1.uuid)
+ end
+
+ it "doesn't change the UUID for exisiting records" do
+ expect { background_migration }.not_to change { finding_with_converted_uuid.uuid_convert_string_to_uuid }
+ end
+ end
+
+ private
+
+ def create_scanner(project, overrides = {})
+ attrs = {
+ project_id: project.id,
+ external_id: "test_vulnerability_scanner",
+ name: "Test Vulnerabilities::Scanner"
+ }.merge(overrides)
+
+ vulnerability_scanners.create!(attrs)
+ end
+
+ def create_identifier(project, overrides = {})
+ attrs = {
+ project_id: project.id,
+ external_id: "CVE-2018-1234",
+ external_type: "CVE",
+ name: "CVE-2018-1234",
+ fingerprint: SecureRandom.hex(20)
+ }.merge(overrides)
+
+ vulnerability_identifiers.create!(attrs)
+ end
+
+ def create_finding(project, scanner, overrides = {})
+ attrs = {
+ project_id: project.id,
+ scanner_id: scanner.id,
+ severity: 5, # medium
+ confidence: 2, # unknown,
+ report_type: 99, # generic
+ primary_identifier_id: create_identifier(project).id,
+ project_fingerprint: SecureRandom.hex(20),
+ location_fingerprint: SecureRandom.hex(20),
+ uuid: SecureRandom.uuid,
+ name: "CVE-2018-1234",
+ raw_metadata: "{}",
+ metadata_version: "test:1.0"
+ }.merge(overrides)
+
+ vulnerability_findings.create!(attrs)
+ end
+
+ def create_user(overrides = {})
+ attrs = {
+ email: "test@example.com",
+ notification_email: "test@example.com",
+ name: "test",
+ username: "test",
+ state: "active",
+ projects_limit: 10
+ }.merge(overrides)
+
+ users.create!(attrs)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
index 37fdd209622..6201e2c0fcc 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy, '#next_batch' do
+RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy,
+ '#next_batch', feature_category: :database do
let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) }
let(:namespaces) { table(:namespaces) }
@@ -64,7 +65,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
context 'when scope has a join which makes the column name ambiguous' do
let(:job_class) do
Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
- scope_to ->(r) { r.joins('LEFT JOIN users ON users.id = namespaces.owner_id') }
+ scope_to ->(r) { r.joins('LEFT JOIN namespaces as parents ON parents.id = namespaces.parent_id') }
end
end
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index c522c8b307f..71e9a568370 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
end
context 'when an upload belongs to a legacy_diff_note' do
- let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:merge_request) { create(:merge_request, :skip_diff_creation, source_project: project) }
let!(:note) do
create(:legacy_diff_note_on_merge_request,
diff --git a/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb b/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb
new file mode 100644
index 00000000000..e3b1b67cb40
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::Redis::BackfillProjectPipelineStatusTtl,
+ :clean_gitlab_redis_cache, feature_category: :redis do
+ let(:redis) { ::Redis.new(::Gitlab::Redis::Cache.params) }
+ let(:keys) { ["cache:gitlab:project:1:pipeline_status", "cache:gitlab:project:2:pipeline_status"] }
+ let(:invalid_keys) { ["cache:gitlab:project:pipeline_status:1", "cache:gitlab:project:pipeline_status:2"] }
+
+ subject { described_class.new }
+
+ before do
+ (keys + invalid_keys).each { |key| redis.set(key, 1) }
+ end
+
+ describe '#perform' do
+ it 'sets a ttl on given keys' do
+ subject.perform(keys)
+
+ keys.each do |k|
+ expect(redis.ttl(k)).to be > 0
+ end
+ end
+ end
+
+ describe '#scan_match_pattern' do
+ it "finds all the required keys only" do
+ expect(redis.scan('0').second).to match_array(keys + invalid_keys)
+ expect(subject.redis.scan_each(match: subject.scan_match_pattern).to_a).to contain_exactly(*keys)
+ end
+ end
+
+ describe '#redis' do
+ it { expect(subject.redis.inspect).to eq(redis.inspect) }
+ end
+end
diff --git a/spec/lib/gitlab/buffered_io_spec.rb b/spec/lib/gitlab/buffered_io_spec.rb
index c6939b819e2..0ec377550c1 100644
--- a/spec/lib/gitlab/buffered_io_spec.rb
+++ b/spec/lib/gitlab/buffered_io_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::BufferedIo do
end
subject(:readuntil) do
- Gitlab::BufferedIo.new(mock_io).readuntil('a', false, start_time)
+ described_class.new(mock_io).readuntil('a', false, start_time)
end
it 'does not raise a timeout error' do
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::BufferedIo do
context 'when not passing start_time' do
subject(:readuntil) do
- Gitlab::BufferedIo.new(mock_io).readuntil('a', false)
+ described_class.new(mock_io).readuntil('a', false)
end
it 'raises a timeout error' do
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index ec0bda3c300..754614bffdb 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -188,9 +188,11 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac
pipeline_status.store_in_cache
read_sha, read_status = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status) }
+ ttl = Gitlab::Redis::Cache.with { |redis| redis.ttl(cache_key) }
expect(read_sha).to eq('123456')
expect(read_status).to eq('failed')
+ expect(ttl).to be > 0
end
end
@@ -254,14 +256,24 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac
end
describe '#load_from_cache' do
+ subject { pipeline_status.load_from_cache }
+
it 'reads the status from redis_cache' do
- pipeline_status.load_from_cache
+ subject
expect(pipeline_status.sha).to eq(sha)
expect(pipeline_status.status).to eq(status)
expect(pipeline_status.ref).to eq(ref)
end
+ it 'refreshes ttl' do
+ subject
+
+ ttl = Gitlab::Redis::Cache.with { |redis| redis.ttl(cache_key) }
+
+ expect(ttl).to be > 0
+ end
+
context 'when status is empty string' do
before do
Gitlab::Redis::Cache.with do |redis|
@@ -271,7 +283,7 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac
end
it 'reads the status as nil' do
- pipeline_status.load_from_cache
+ subject
expect(pipeline_status.status).to eq(nil)
end
diff --git a/spec/lib/gitlab/cache/client_spec.rb b/spec/lib/gitlab/cache/client_spec.rb
index 638fed1a905..6543381b86a 100644
--- a/spec/lib/gitlab/cache/client_spec.rb
+++ b/spec/lib/gitlab/cache/client_spec.rb
@@ -3,52 +3,32 @@
require 'spec_helper'
RSpec.describe Gitlab::Cache::Client, feature_category: :source_code_management do
- subject(:client) { described_class.new(metadata, backend: backend) }
+ subject(:client) { described_class.new(metrics, backend: backend) }
+ let(:metrics) { Gitlab::Cache::Metrics.new(metadata) }
let(:backend) { Rails.cache }
- let(:metadata) do
- Gitlab::Cache::Metadata.new(
- cache_identifier: cache_identifier,
- feature_category: feature_category,
- backing_resource: backing_resource
- )
- end
let(:cache_identifier) { 'MyClass#cache' }
let(:feature_category) { :source_code_management }
let(:backing_resource) { :cpu }
- let(:metadata_mock) do
+ let(:metadata) do
Gitlab::Cache::Metadata.new(
cache_identifier: cache_identifier,
- feature_category: feature_category
+ feature_category: feature_category,
+ backing_resource: backing_resource
)
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 = {
- 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
+ let(:labels) do
+ {
+ feature_category: :audit_events
+ }
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
@@ -56,9 +36,9 @@ RSpec.describe Gitlab::Cache::Client, feature_category: :source_code_management
end
it 'increments cache miss' do
- expect(metrics_mock).to receive(:increment_cache_miss)
+ expect(metrics).to receive(:increment_cache_miss).with(labels).and_call_original
- client.read('key')
+ expect(client.read('key', nil, labels)).to be_nil
end
end
@@ -72,9 +52,9 @@ RSpec.describe Gitlab::Cache::Client, feature_category: :source_code_management
end
it 'increments cache hit' do
- expect(metrics_mock).to receive(:increment_cache_hit)
+ expect(metrics).to receive(:increment_cache_hit).with(labels)
- client.read('key')
+ expect(client.read('key', nil, labels)).to eq('value')
end
end
end
@@ -125,13 +105,13 @@ RSpec.describe Gitlab::Cache::Client, feature_category: :source_code_management
end
it 'increments a cache hit' do
- expect(metrics_mock).to receive(:increment_cache_hit)
+ expect(metrics).to receive(:increment_cache_hit).with(labels)
- client.fetch('key')
+ expect(client.fetch('key', nil, labels)).to eq('value')
end
it 'does not measure the cache generation time' do
- expect(metrics_mock).not_to receive(:observe_cache_generation)
+ expect(metrics).not_to receive(:observe_cache_generation)
client.fetch('key') { 'new-value' }
end
@@ -145,15 +125,15 @@ RSpec.describe Gitlab::Cache::Client, feature_category: :source_code_management
end
it 'increments a cache miss' do
- expect(metrics_mock).to receive(:increment_cache_miss)
+ expect(metrics).to receive(:increment_cache_miss).with(labels)
- client.fetch('key')
+ expect(client.fetch('key', nil, labels) { 'value' }).to eq('value')
end
it 'measures the cache generation time' do
- expect(metrics_mock).to receive(:observe_cache_generation)
+ expect(metrics).to receive(:observe_cache_generation).with(labels).and_call_original
- client.fetch('key') { 'value' }
+ expect(client.fetch('key', nil, labels) { 'value' }).to eq('value')
end
end
end
diff --git a/spec/lib/gitlab/cache/metadata_spec.rb b/spec/lib/gitlab/cache/metadata_spec.rb
index d2b79fb8b08..82005915c22 100644
--- a/spec/lib/gitlab/cache/metadata_spec.rb
+++ b/spec/lib/gitlab/cache/metadata_spec.rb
@@ -18,12 +18,11 @@ RSpec.describe Gitlab::Cache::Metadata, feature_category: :source_code_managemen
describe '#initialize' do
context 'when optional arguments are not set' do
it 'sets default value for them' do
- attributes = described_class.new(
- cache_identifier: cache_identifier,
- feature_category: feature_category
- )
+ attributes = described_class.new
+ expect(attributes.feature_category).to eq(:not_owned)
expect(attributes.backing_resource).to eq(:unknown)
+ expect(attributes.cache_identifier).to be_nil
end
end
@@ -44,6 +43,12 @@ RSpec.describe Gitlab::Cache::Metadata, feature_category: :source_code_managemen
end
end
+ context 'when not_owned feature category is set' do
+ let(:feature_category) { :not_owned }
+
+ it { expect(attributes.feature_category).to eq(:not_owned) }
+ end
+
context 'when backing resource is not supported' do
let(:backing_resource) { 'foo' }
diff --git a/spec/lib/gitlab/cache/metrics_spec.rb b/spec/lib/gitlab/cache/metrics_spec.rb
index 76ec0dbfa0b..c46533371d3 100644
--- a/spec/lib/gitlab/cache/metrics_spec.rb
+++ b/spec/lib/gitlab/cache/metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Cache::Metrics do
+RSpec.describe Gitlab::Cache::Metrics, feature_category: :source_code_management do
subject(:metrics) { described_class.new(metadata) }
let(:metadata) do
@@ -23,12 +23,19 @@ RSpec.describe Gitlab::Cache::Metrics do
allow(Gitlab::Metrics).to receive(:counter)
.with(
:redis_hit_miss_operations_total,
- 'Hit/miss Redis cache counter'
+ 'Hit/miss Redis cache counter',
+ {
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ }
).and_return(counter_mock)
end
describe '#increment_cache_hit' do
- subject { metrics.increment_cache_hit }
+ subject { metrics.increment_cache_hit(labels) }
+
+ let(:labels) { {} }
it 'increments number of hits' do
expect(counter_mock)
@@ -44,10 +51,31 @@ RSpec.describe Gitlab::Cache::Metrics do
subject
end
+
+ context 'when labels redefine defaults' do
+ let(:labels) { { backing_resource: :gitaly } }
+
+ it 'increments number of hits' do
+ expect(counter_mock)
+ .to receive(:increment)
+ .with(
+ {
+ backing_resource: :gitaly,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ cache_hit: true
+ }
+ ).once
+
+ subject
+ end
+ end
end
describe '#increment_cache_miss' do
- subject { metrics.increment_cache_miss }
+ subject { metrics.increment_cache_miss(labels) }
+
+ let(:labels) { {} }
it 'increments number of misses' do
expect(counter_mock)
@@ -63,22 +91,40 @@ RSpec.describe Gitlab::Cache::Metrics do
subject
end
+
+ context 'when labels redefine defaults' do
+ let(:labels) { { backing_resource: :gitaly } }
+
+ it 'increments number of misses' do
+ expect(counter_mock)
+ .to receive(:increment)
+ .with(
+ {
+ backing_resource: :gitaly,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ cache_hit: false
+ }
+ ).once
+
+ subject
+ end
+ end
end
describe '#observe_cache_generation' do
subject do
- metrics.observe_cache_generation { action }
+ metrics.observe_cache_generation(labels) { action }
end
let(:action) { 'action' }
let(:histogram_mock) { instance_double(Prometheus::Client::Histogram) }
+ let(:labels) { {} }
before do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100.0, 500.0)
- end
- it 'updates histogram metric' do
- expect(Gitlab::Metrics).to receive(:histogram).with(
+ allow(Gitlab::Metrics).to receive(:histogram).with(
:redis_cache_generation_duration_seconds,
'Duration of Redis cache generation',
{
@@ -88,10 +134,36 @@ RSpec.describe Gitlab::Cache::Metrics do
},
[0, 1, 5]
).and_return(histogram_mock)
+ end
- expect(histogram_mock).to receive(:observe).with({}, 400.0)
+ it 'updates histogram metric' do
+ expect(histogram_mock).to receive(:observe).with(
+ {
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ },
+ 400.0
+ )
is_expected.to eq(action)
end
+
+ context 'when labels redefine defaults' do
+ let(:labels) { { backing_resource: :gitaly } }
+
+ it 'updates histogram metric' do
+ expect(histogram_mock).to receive(:observe).with(
+ {
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: :gitaly
+ },
+ 400.0
+ )
+
+ is_expected.to eq(action)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
index 552afcdb180..854c04dd581 100644
--- a/spec/lib/gitlab/checks/changes_access_spec.rb
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -24,6 +24,14 @@ RSpec.describe Gitlab::Checks::ChangesAccess, feature_category: :source_code_man
subject.validate!
end
+
+ it 'calls file size check' do
+ expect_next_instance_of(Gitlab::Checks::GlobalFileSizeCheck) do |instance|
+ expect(instance).to receive(:validate!)
+ end
+
+ subject.validate!
+ end
end
context 'when time limit was reached' do
diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb
index dd467537a4f..20c6ad8a6e8 100644
--- a/spec/lib/gitlab/checks/diff_check_spec.rb
+++ b/spec/lib/gitlab/checks/diff_check_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe Gitlab::Checks::DiffCheck, feature_category: :source_code_managem
context 'when change is sent by a different user' do
it 'raises an error if the user is not allowed to update the file' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.username}")
end
end
@@ -148,7 +148,7 @@ RSpec.describe Gitlab::Checks::DiffCheck, feature_category: :source_code_managem
end
it "does raise an error" do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'files/locked/baz.lfs' is locked in Git LFS by #{owner.name}")
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'files/locked/baz.lfs' is locked in Git LFS by #{owner.username}")
end
end
end
diff --git a/spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb b/spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb
new file mode 100644
index 00000000000..3b52d2e1364
--- /dev/null
+++ b/spec/lib/gitlab/checks/file_size_check/allow_existing_oversized_blobs_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Checks::FileSizeCheck::AllowExistingOversizedBlobs, feature_category: :source_code_management do
+ subject { checker.find }
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:checker) do
+ described_class.new(
+ project: project,
+ changes: changes,
+ file_size_limit_megabytes: 1)
+ end
+
+ describe '#find' do
+ let(:branch_name) { SecureRandom.uuid }
+ let(:other_branch_name) { SecureRandom.uuid }
+ let(:filename) { 'log.log' }
+ let(:create_file) do
+ project.repository.create_file(
+ project.owner,
+ filename,
+ initial_contents,
+ branch_name: branch_name,
+ message: 'whatever'
+ )
+ end
+
+ let(:changed_ref) do
+ project.repository.update_file(
+ project.owner,
+ filename,
+ changed_contents,
+ branch_name: other_branch_name,
+ start_branch_name: branch_name,
+ message: 'whatever'
+ )
+ end
+
+ let(:changes) { [oldrev: create_file, newrev: changed_ref] }
+
+ before do
+ # set up a branch
+ create_file
+
+ # branch off that branch
+ changed_ref
+
+ # delete stuff so it can be picked up by new_blobs
+ project.repository.delete_branch(other_branch_name)
+ end
+
+ context 'when changing from valid to oversized' do
+ let(:initial_contents) { 'a' }
+ let(:changed_contents) { 'a' * ((2**20) + 1) } # 1 MB + 1 byte
+
+ it 'returns an array with blobs that became oversized' do
+ blob = subject.first
+ expect(blob.path).to eq(filename)
+ expect(subject).to contain_exactly(blob)
+ end
+ end
+
+ context 'when changing from oversized to oversized' do
+ let(:initial_contents) { 'a' * ((2**20) + 1) } # 1 MB + 1 byte
+ let(:changed_contents) { 'a' * ((2**20) + 2) } # 1 MB + 1 byte
+
+ it { is_expected.to be_blank }
+ end
+
+ context 'when changing from oversized to valid' do
+ let(:initial_contents) { 'a' * ((2**20) + 1) } # 1 MB + 1 byte
+ let(:changed_contents) { 'aa' }
+
+ it { is_expected.to be_blank }
+ end
+
+ context 'when changing from valid to valid' do
+ let(:initial_contents) { 'abc' }
+ let(:changed_contents) { 'def' }
+
+ it { is_expected.to be_blank }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/file_size_check/any_oversized_blobs_spec.rb b/spec/lib/gitlab/checks/file_size_check/any_oversized_blobs_spec.rb
new file mode 100644
index 00000000000..7c786c2ba24
--- /dev/null
+++ b/spec/lib/gitlab/checks/file_size_check/any_oversized_blobs_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Checks::FileSizeCheck::AnyOversizedBlobs, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:any_blob) do
+ described_class.new(
+ project: project,
+ changes: [{ newrev: 'bf12d2567099e26f59692896f73ac819bae45b00' }],
+ file_size_limit_megabytes: 1)
+ end
+
+ describe '#find' do
+ subject { any_blob.find }
+
+ # SHA of the 2-mb-file branch
+ let(:newrev) { 'bf12d2567099e26f59692896f73ac819bae45b00' }
+ let(:timeout) { nil }
+
+ before do
+ # Delete branch so Repository#new_blobs can return results
+ project.repository.delete_branch('2-mb-file')
+ end
+
+ it 'returns the blob exceeding the file size limit' do
+ expect(subject).to contain_exactly(kind_of(Gitlab::Git::Blob))
+ expect(subject[0].path).to eq('file.bin')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/global_file_size_check_spec.rb b/spec/lib/gitlab/checks/global_file_size_check_spec.rb
new file mode 100644
index 00000000000..9ea0c73b1c7
--- /dev/null
+++ b/spec/lib/gitlab/checks/global_file_size_check_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Checks::GlobalFileSizeCheck, feature_category: :source_code_management do
+ include_context 'changes access checks context'
+
+ describe '#validate!' do
+ context 'when global_file_size_check is disabled' do
+ before do
+ stub_feature_flags(global_file_size_check: false)
+ end
+
+ it 'does not log' do
+ expect(subject).not_to receive(:log_timed)
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+ expect(Gitlab::Checks::FileSizeCheck::AllowExistingOversizedBlobs).not_to receive(:new)
+ subject.validate!
+ end
+ end
+
+ it 'checks for file sizes' do
+ expect_next_instance_of(Gitlab::Checks::FileSizeCheck::AllowExistingOversizedBlobs,
+ project: project,
+ changes: changes,
+ file_size_limit_megabytes: 100
+ ) do |check|
+ expect(check).to receive(:find).and_call_original
+ end
+ expect(subject.logger).to receive(:log_timed).with('Checking for blobs over the file size limit')
+ .and_call_original
+ expect(Gitlab::AppJsonLogger).to receive(:info).with('Checking for blobs over the file size limit')
+ subject.validate!
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/snippet_check_spec.rb b/spec/lib/gitlab/checks/snippet_check_spec.rb
index 89417aaca4d..c43b65d09c5 100644
--- a/spec/lib/gitlab/checks/snippet_check_spec.rb
+++ b/spec/lib/gitlab/checks/snippet_check_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Checks::SnippetCheck do
let(:creation) { false }
let(:deletion) { false }
- subject { Gitlab::Checks::SnippetCheck.new(changes, default_branch: default_branch, root_ref: snippet.repository.root_ref, logger: logger) }
+ subject { described_class.new(changes, default_branch: default_branch, root_ref: snippet.repository.root_ref, logger: logger) }
describe '#validate!' do
it 'does not raise any error' do
diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
index 76a596e1db3..ff918bbe558 100644
--- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
+++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::ArtifactFileReader do
+RSpec.describe Gitlab::Ci::ArtifactFileReader, feature_category: :pipeline_composition do
let(:job) { create(:ci_build) }
let(:path) { 'generated.yml' } # included in the ci_build_artifacts.zip
@@ -138,8 +138,8 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do
end
context 'when job does not have artifacts' do
- it 'raises ArgumentError' do
- expect { subject }.to raise_error(ArgumentError, 'Job does not have artifacts')
+ it 'raises an Error' do
+ expect { subject }.to raise_error(described_class::Error, 'Job does not have artifacts')
end
end
end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index 9f191fed581..99577539798 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -244,59 +244,6 @@ RSpec.describe Gitlab::Ci::Build::Rules, feature_category: :pipeline_composition
when: 'never' }]))
}
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(introduce_rules_with_needs: false)
- end
-
- context 'with needs' do
- context 'when single need is specified' do
- let(:rule_list) do
- [{ if: '$VAR == null', needs: [{ name: 'test', artifacts: true, optional: false }] }]
- end
-
- it {
- is_expected.to eq(described_class::Result.new(when: 'on_success'))
- }
- end
-
- context 'when multiple needs are specified' do
- let(:rule_list) do
- [{ if: '$VAR == null',
- needs: [{ name: 'test', artifacts: true, optional: false },
- { name: 'rspec', artifacts: true, optional: false }] }]
- end
-
- it {
- is_expected.to eq(described_class::Result.new(when: 'on_success'))
- }
- end
-
- context 'when there are no needs specified' do
- let(:rule_list) { [{ if: '$VAR == null' }] }
-
- it {
- is_expected.to eq(described_class::Result.new(when: 'on_success'))
- }
- end
-
- context 'when need is specified with additional attibutes' do
- let(:rule_list) do
- [{ if: '$VAR == null', needs: [{
- artifacts: false,
- name: 'test',
- optional: true,
- when: 'never'
- }] }]
- end
-
- it {
- is_expected.to eq(described_class::Result.new(when: 'on_success'))
- }
- end
- end
- end
end
context 'with variables' do
diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb
index b80422d03e5..511036efd37 100644
--- a/spec/lib/gitlab/ci/components/instance_path_spec.rb
+++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb
@@ -101,30 +101,22 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
context 'when version is `~latest`' do
let(:version) { '~latest' }
- context 'when project is a catalog resource' do
- before do
- create(:catalog_resource, project: existing_project)
+ context 'when project has releases' do
+ let_it_be(:latest_release) do
+ create(:release, project: existing_project, sha: 'sha-1', released_at: Time.zone.now)
end
- context 'when project has releases' do
- let_it_be(:releases) do
- [
- create(:release, project: existing_project, sha: 'sha-1', released_at: Time.zone.now - 1.day),
- create(:release, project: existing_project, sha: 'sha-2', released_at: Time.zone.now)
- ]
- end
-
- it 'returns the sha of the latest release' do
- expect(path.sha).to eq(releases.last.sha)
- end
+ before(:all) do
+ # Previous release
+ create(:release, project: existing_project, sha: 'sha-2', released_at: Time.zone.now - 1.day)
end
- context 'when project does not have releases' do
- it { expect(path.sha).to be_nil }
+ it 'returns the sha of the latest release' do
+ expect(path.sha).to eq(latest_release.sha)
end
end
- context 'when project is not a catalog resource' do
+ context 'when project does not have releases' do
it { expect(path.sha).to be_nil }
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 1f4586bd5a9..c7f18f0d01a 100644
--- a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
@@ -41,6 +41,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :
it 'sets the expected error' do
expect(valid?).to be_falsy
expect(external_file.errors).to contain_exactly(expected_error)
+ expect(external_file.content).to eq(nil)
end
end
@@ -139,7 +140,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :
before do
allow_next_instance_of(Gitlab::Ci::ArtifactFileReader) do |reader|
- allow(reader).to receive(:read).and_return('')
+ allow(reader).to receive(:read).and_return(nil)
end
end
@@ -165,8 +166,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :
}
expect(context).to receive(:mutate).with(expected_attrs).and_call_original
- expect(valid?).to be_truthy
external_file.content
+ expect(valid?).to be_truthy
end
end
end
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 1c5918f77ca..d6dd75f4b10 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
it 'is not a valid file' do
expect(valid?).to be_falsy
expect(file.error_message)
- .to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: content does not have a valid YAML syntax')
+ .to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: Invalid configuration format')
end
end
@@ -128,31 +128,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
end
end
- context 'when interpolation is disabled but there is a spec header' do
- before do
- stub_feature_flags(ci_includable_files_interpolation: false)
- end
-
- let(:location) { 'some-location.yml' }
-
- let(:content) do
- <<~YAML
- spec:
- include:
- website:
- ---
- run:
- script: deploy $[[ inputs.website ]]
- YAML
- end
-
- it 'returns an error saying that interpolation is disabled' do
- expect(valid?).to be_falsy
- expect(file.errors)
- .to include('`some-location.yml`: can not evaluate included file because interpolation is disabled')
- end
- end
-
context 'when interpolation was unsuccessful' do
let(:location) { 'some-location.yml' }
@@ -275,4 +250,17 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
it { is_expected.to eq([{ location: location, content: content }, nil, 'HEAD'].hash) }
end
end
+
+ describe '#load_and_validate_expanded_hash!' do
+ let(:location) { 'some/file/config.yml' }
+ let(:logger) { instance_double(::Gitlab::Ci::Pipeline::Logger, :instrument) }
+ let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger } }
+
+ it 'includes instrumentation for loading and expanding the content' do
+ expect(logger).to receive(:instrument).once.ordered.with(:config_file_fetch_content_hash).and_yield
+ expect(logger).to receive(:instrument).once.ordered.with(:config_file_expand_content_includes).and_yield
+
+ file.load_and_validate_expanded_hash!
+ end
+ end
end
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 fe811bce9fe..7e3406413d0 100644
--- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
@@ -121,7 +121,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
it 'is invalid' do
expect(subject).to be_falsy
- expect(external_resource.error_message).to match(/does not have a valid YAML syntax/)
+ expect(external_resource.error_message).to match(/Invalid configuration format/)
end
end
end
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 4da3e7e51a7..1a2a6c5beeb 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
@@ -29,18 +29,5 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :
[{ local: 'config/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1' }] }]
)
end
-
- context 'when FF `ci_support_include_rules_when_never` is disabled' do
- before do
- stub_feature_flags(ci_support_include_rules_when_never: false)
- end
-
- it 'filters locations according to rules ignoring when:' do
- is_expected.to eq(
- [{ local: 'config/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1' }] },
- { remote: 'https://testing.com/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1', when: 'never' }] }]
- )
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 74afb3b1e97..935b6989dd7 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
- '`lib/gitlab/ci/templates/template.yml`: content does not have a valid YAML syntax'
+ '`lib/gitlab/ci/templates/template.yml`: Invalid configuration format'
)
end
end
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
index 1ba5caa1d4b..25b7998ef5e 100644
--- a/spec/lib/gitlab/ci/config/external/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -3,8 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do
- # Remove `project` property when FF `ci_support_include_rules_when_never` is removed
- let(:context) { double(variables_hash: {}, project: nil) }
+ let(:context) { double(variables_hash: {}) }
let(:rule_hashes) { [{ if: '$MY_VAR == "hello"' }] }
subject(:rules) { described_class.new(rule_hashes) }
@@ -19,11 +18,6 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_
end
shared_examples 'when there is a rule with if' do |rule_matched_result = true, rule_not_matched_result = false|
- # Remove this `before` block when FF `ci_support_include_rules_when_never` is removed
- before do
- allow(context).to receive(:project).and_return(nil)
- end
-
context 'when the rule matches' do
let(:context) { double(variables_hash: { 'MY_VAR' => 'hello' }) }
@@ -70,28 +64,12 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_
let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'never' }] }
it_behaves_like 'when there is a rule with if', false, false
-
- context 'when FF `ci_support_include_rules_when_never` is disabled' do
- before do
- stub_feature_flags(ci_support_include_rules_when_never: false)
- end
-
- it_behaves_like 'when there is a rule with if'
- end
end
context 'with when: always' do
let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'always' }] }
it_behaves_like 'when there is a rule with if'
-
- context 'when FF `ci_support_include_rules_when_never` is disabled' do
- before do
- stub_feature_flags(ci_support_include_rules_when_never: false)
- end
-
- it_behaves_like 'when there is a rule with if'
- end
end
context 'with when: <invalid string>' do
@@ -115,28 +93,12 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_
let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'never' }] }
it_behaves_like 'when there is a rule with exists', false, false
-
- context 'when FF `ci_support_include_rules_when_never` is disabled' do
- before do
- stub_feature_flags(ci_support_include_rules_when_never: false)
- end
-
- it_behaves_like 'when there is a rule with exists'
- end
end
context 'with when: always' do
let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'always' }] }
it_behaves_like 'when there is a rule with exists'
-
- context 'when FF `ci_support_include_rules_when_never` is disabled' do
- before do
- stub_feature_flags(ci_support_include_rules_when_never: false)
- end
-
- it_behaves_like 'when there is a rule with exists'
- end
end
context 'with when: <invalid string>' do
diff --git a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb b/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb
index 726ed6d95a0..888756a3eb1 100644
--- a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb
@@ -5,10 +5,10 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project) }
- let(:ctx) { instance_double(Gitlab::Ci::Config::External::Context, project: project, user: build(:user, id: 1234)) }
+ let(:current_user) { build(:user, id: 1234) }
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) }
- subject { described_class.new(result, arguments, ctx) }
+ subject { described_class.new(result, arguments, current_user: current_user) }
context 'when input data is valid' do
let(:header) do
@@ -39,7 +39,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end
context 'when config has a syntax error' do
- let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new) }
+ let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }
let(:arguments) do
{ website: 'gitlab.com' }
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
expect(subject).not_to be_valid
expect(subject.error_message).to eq subject.errors.first
- expect(subject.errors).to include 'content does not have a valid YAML syntax'
+ expect(subject.errors).to include 'Invalid configuration format'
end
end
@@ -142,28 +142,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end
describe '#to_hash' do
- context 'when interpolation is disabled' do
- before do
- stub_feature_flags(ci_includable_files_interpolation: false)
- end
-
- let(:header) do
- { spec: { inputs: { website: nil } } }
- end
-
- let(:content) do
- { test: 'deploy $[[ inputs.website ]]' }
- end
-
- let(:arguments) { {} }
-
- it 'returns an empty hash' do
- subject.interpolate!
-
- expect(subject.to_hash).to be_empty
- end
- end
-
context 'when interpolation is not used' do
let(:result) do
::Gitlab::Ci::Config::Yaml::Result.new(config: content)
@@ -202,118 +180,4 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end
end
end
-
- describe '#ready?' do
- let(:header) do
- { spec: { inputs: { website: nil } } }
- end
-
- let(:content) do
- { test: 'deploy $[[ inputs.website ]]' }
- end
-
- let(:arguments) do
- { website: 'gitlab.com' }
- end
-
- it 'returns false if interpolation has not been done yet' do
- expect(subject).not_to be_ready
- end
-
- it 'returns true if interpolation has been performed' do
- subject.interpolate!
-
- expect(subject).to be_ready
- end
-
- context 'when interpolation can not be performed' do
- let(:result) do
- ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new)
- end
-
- it 'returns true if interpolator has preliminary errors' do
- expect(subject).to be_ready
- end
-
- it 'returns true if interpolation has been attempted' do
- subject.interpolate!
-
- expect(subject).to be_ready
- end
- end
- end
-
- describe '#interpolate?' do
- let(:header) do
- { spec: { inputs: { website: nil } } }
- end
-
- let(:content) do
- { test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
- end
-
- let(:arguments) do
- { website: 'gitlab.com' }
- end
-
- context 'when interpolation can be performed' do
- it 'will perform interpolation' do
- expect(subject.interpolate?).to eq true
- end
- end
-
- context 'when interpolation is disabled' do
- before do
- stub_feature_flags(ci_includable_files_interpolation: false)
- end
-
- it 'will not perform interpolation' do
- expect(subject.interpolate?).to eq false
- end
- end
-
- context 'when an interpolation header is missing' do
- let(:header) { nil }
-
- it 'will not perform interpolation' do
- expect(subject.interpolate?).to eq false
- end
- end
-
- context 'when interpolator has preliminary errors' do
- let(:result) do
- ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new)
- end
-
- it 'will not perform interpolation' do
- expect(subject.interpolate?).to eq false
- end
- end
- end
-
- describe '#has_header?' do
- let(:content) do
- { test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
- end
-
- let(:arguments) do
- { website: 'gitlab.com' }
- end
-
- context 'when header is an empty hash' do
- let(:header) { {} }
-
- it 'does not have a header available' do
- expect(subject).not_to have_header
- end
- end
-
- context 'when header is not specified' do
- let(:header) { nil }
-
- it 'does not have a header available' do
- expect(subject).not_to have_header
- end
- end
- end
end
diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
index 1e417bcd8af..4e6151677e6 100644
--- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
@@ -2,151 +2,58 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_composition do
- describe '#to_result' do
+RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_composition do
+ describe '#load' do
let_it_be(:project) { create(:project) }
- subject(:result) { described_class.new(yaml, project: project).to_result }
-
- context 'when syntax is invalid' do
- let(:yaml) { 'some: invalid: syntax' }
-
- it 'returns an invalid result object' do
- expect(result).not_to be_valid
- expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
- end
+ let(:inputs) { { test_input: 'hello test' } }
+
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ ---
+ test_job:
+ script:
+ - echo "$[[ inputs.test_input ]]"
+ YAML
end
- context 'when the first document is a header' do
- context 'with explicit document start marker' do
- let(:yaml) do
- <<~YAML
- ---
- spec:
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as header and the second as content' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result.header).to eq({ spec: nil })
- expect(result.content).to eq({ b: 2 })
- end
- end
- end
+ subject(:result) { described_class.new(yaml, inputs: inputs, current_user: project.creator).load }
- context 'when first document is empty' do
- let(:yaml) do
- <<~YAML
- ---
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as header and the second as content' do
- expect(result).not_to have_header
- end
- end
-
- context 'when first document is an empty hash' do
- let(:yaml) do
- <<~YAML
- {}
- ---
- b: 2
- YAML
- end
+ it 'loads and interpolates CI config YAML' do
+ expected_config = { test_job: { script: ['echo "hello test"'] } }
- it 'returns second document as a content' do
- expect(result).not_to have_header
- expect(result.content).to eq({ b: 2 })
- end
+ expect(result).to be_valid
+ expect(result.content).to eq(expected_config)
end
- context 'when first an array' do
- let(:yaml) do
- <<~YAML
- ---
- - a
- - b
- ---
- b: 2
- YAML
- end
+ it 'allows the use of YAML reference tags' do
+ expect(Psych).to receive(:add_tag).once.with(
+ ::Gitlab::Ci::Config::Yaml::Tags::Reference.tag,
+ ::Gitlab::Ci::Config::Yaml::Tags::Reference
+ )
- it 'considers the first document as header and the second as content' do
- expect(result).not_to have_header
- end
+ result
end
- context 'when the first document is not a header' do
- let(:yaml) do
- <<~YAML
- a: 1
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as content for backwards compatibility' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
-
- context 'with explicit document start marker' do
- let(:yaml) do
- <<~YAML
- ---
- a: 1
- ---
- b: 2
- YAML
- end
+ context 'when there is an error loading the YAML' do
+ let(:yaml) { 'invalid...yaml' }
- it 'considers the first document as content for backwards compatibility' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
+ it 'returns an error result' do
+ expect(result).not_to be_valid
+ expect(result.error).to eq('Invalid configuration format')
end
end
- context 'when the first document is not a header and second document is empty' do
- let(:yaml) do
- <<~YAML
- a: 1
- ---
- YAML
- end
-
- it 'considers the first document as content' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
-
- context 'with explicit document start marker' do
- let(:yaml) do
- <<~YAML
- ---
- a: 1
- ---
- YAML
- end
+ context 'when there is an error interpolating the YAML' do
+ let(:inputs) { {} }
- it 'considers the first document as content' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
+ it 'returns an error result' do
+ expect(result).not_to be_valid
+ expect(result.error).to eq('`test_input` input: required value has not been provided')
end
end
end
diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb
index 3576dd481c6..27d93d555f1 100644
--- a/spec/lib/gitlab/ci/config/yaml_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml_spec.rb
@@ -3,18 +3,20 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do
- describe '.load!' do
- it 'loads a YAML file' do
- yaml = <<~YAML
- image: 'image:1.0'
- texts:
- nested_key: 'value1'
- more_text:
- more_nested_key: 'value2'
- YAML
+ let(:yaml) do
+ <<~YAML
+ image: 'image:1.0'
+ texts:
+ nested_key: 'value1'
+ more_text:
+ more_nested_key: 'value2'
+ YAML
+ end
- config = described_class.load!(yaml)
+ describe '.load!' do
+ subject(:config) { described_class.load!(yaml) }
+ it 'loads a YAML file' do
expect(config).to eq({
image: 'image:1.0',
texts: {
@@ -30,156 +32,20 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition
let(:yaml) { 'some: invalid: syntax' }
it 'raises an error' do
- expect { described_class.load!(yaml) }
+ expect { config }
.to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/
end
end
- end
-
- describe '.load_result!' do
- let_it_be(:project) { create(:project) }
-
- subject(:result) { described_class.load_result!(yaml, project: project) }
-
- context 'when syntax is invalid' do
- let(:yaml) { 'some: invalid: syntax' }
-
- it 'returns an invalid result object' do
- expect(result).not_to be_valid
- expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
- end
- end
-
- context 'when the first document is a header' do
- context 'with explicit document start marker' do
- let(:yaml) do
- <<~YAML
- ---
- spec:
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as header and the second as content' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result.header).to eq({ spec: nil })
- expect(result.content).to eq({ b: 2 })
- end
- end
- end
-
- context 'when first document is empty' do
- let(:yaml) do
- <<~YAML
- ---
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as header and the second as content' do
- expect(result).not_to have_header
- end
- end
-
- context 'when first document is an empty hash' do
- let(:yaml) do
- <<~YAML
- {}
- ---
- b: 2
- YAML
- end
- it 'returns second document as a content' do
- expect(result).not_to have_header
- expect(result.content).to eq({ b: 2 })
- end
- end
+ context 'when given a user' do
+ let(:user) { instance_double(User) }
- context 'when first an array' do
- let(:yaml) do
- <<~YAML
- ---
- - a
- - b
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as header and the second as content' do
- expect(result).not_to have_header
- end
- end
-
- context 'when the first document is not a header' do
- let(:yaml) do
- <<~YAML
- a: 1
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as content for backwards compatibility' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
-
- context 'with explicit document start marker' do
- let(:yaml) do
- <<~YAML
- ---
- a: 1
- ---
- b: 2
- YAML
- end
-
- it 'considers the first document as content for backwards compatibility' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
- end
- end
-
- context 'when the first document is not a header and second document is empty' do
- let(:yaml) do
- <<~YAML
- a: 1
- ---
- YAML
- end
-
- it 'considers the first document as content' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
+ subject(:config) { described_class.load!(yaml, current_user: user) }
- context 'with explicit document start marker' do
- let(:yaml) do
- <<~YAML
- ---
- a: 1
- ---
- YAML
- end
+ it 'passes it to Loader' do
+ expect(::Gitlab::Ci::Config::Yaml::Loader).to receive(:new).with(yaml, current_user: user).and_call_original
- it 'considers the first document as content' do
- expect(result).to be_valid
- expect(result.error).to be_nil
- expect(result).not_to have_header
- expect(result.content).to eq({ a: 1 })
- end
+ config
end
end
end
diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb
index 15be67329a8..575f174f737 100644
--- a/spec/lib/gitlab/ci/jwt_v2_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb
@@ -62,6 +62,23 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
end
describe 'custom claims' do
+ let(:project_config) do
+ instance_double(
+ Gitlab::Ci::ProjectConfig,
+ url: 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml',
+ source: :repository_source
+ )
+ end
+
+ before do
+ allow(Gitlab::Ci::ProjectConfig).to receive(:new).with(
+ project: project,
+ sha: pipeline.sha,
+ pipeline_source: pipeline.source.to_sym,
+ pipeline_source_bridge: pipeline.source_bridge
+ ).and_return(project_config)
+ end
+
describe 'runner_id' do
it 'is the ID of the runner executing the job' do
expect(payload[:runner_id]).to eq(runner.id)
@@ -113,23 +130,6 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
end
describe 'ci_config_ref_uri' do
- let(:project_config) do
- instance_double(
- Gitlab::Ci::ProjectConfig,
- url: 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml',
- source: :repository_source
- )
- end
-
- before do
- allow(Gitlab::Ci::ProjectConfig).to receive(:new).with(
- project: project,
- sha: pipeline.sha,
- pipeline_source: pipeline.source.to_sym,
- pipeline_source_bridge: pipeline.source_bridge
- ).and_return(project_config)
- end
-
it 'joins project_config.url and pipeline.source_ref_path with @' do
expect(payload[:ci_config_ref_uri]).to eq('gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' \
'@refs/heads/auto-deploy-2020-03-19')
@@ -165,15 +165,31 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
end
end
- context 'when ci_jwt_v2_ci_config_ref_uri_claim flag is disabled' do
+ context 'when config source is not repository' do
before do
- stub_feature_flags(ci_jwt_v2_ref_uri_claim: false)
+ allow(project_config).to receive(:source).and_return(:auto_devops_source)
end
it 'is nil' do
expect(payload[:ci_config_ref_uri]).to be_nil
end
end
+ end
+
+ describe 'ci_config_sha' do
+ it 'is the SHA of the pipeline' do
+ expect(payload[:ci_config_sha]).to eq(pipeline.sha)
+ end
+
+ context 'when project config is nil' do
+ before do
+ allow(Gitlab::Ci::ProjectConfig).to receive(:new).and_return(nil)
+ end
+
+ it 'is nil' do
+ expect(payload[:ci_config_sha]).to be_nil
+ end
+ end
context 'when config source is not repository' do
before do
@@ -181,7 +197,7 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do
end
it 'is nil' do
- expect(payload[:ci_config_ref_uri]).to be_nil
+ expect(payload[:ci_config_sha]).to be_nil
end
end
end
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 a9a52972294..9c268d9039e 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: :
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)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
index 63b8e5fdf01..c1eaea511b7 100644
--- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
@@ -24,4 +24,28 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Source, feature_category: :dependency_
data: attributes[:data]
)
end
+
+ describe '#source_file_path' do
+ it 'returns the correct source_file_path' do
+ expect(subject.source_file_path).to eq('package.json')
+ end
+ end
+
+ describe '#input_file_path' do
+ it 'returns the correct input_file_path' do
+ expect(subject.input_file_path).to eq("package-lock.json")
+ end
+ end
+
+ describe '#packager' do
+ it 'returns the correct package manager name' do
+ expect(subject.packager).to eq("npm")
+ end
+ end
+
+ describe '#language' do
+ it 'returns the correct langauge' do
+ expect(subject.language).to eq("JavaScript")
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 35d44281072..702341a7ea7 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::Stage::Factory do
+RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory do
end
context 'when stage has manual builds' do
- (Ci::HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status|
+ Ci::HasStatus::BLOCKED_STATUS.each do |core_status|
context "when status is #{core_status}" do
let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) }
diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
index 9fdaddc083e..e23645c106b 100644
--- a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do
+RSpec.describe Gitlab::Ci::Status::Stage::PlayManual, feature_category: :continuous_integration do
let(:stage) { double('stage') }
let(:play_manual) { described_class.new(stage) }
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do
context 'when stage is skipped' do
let(:stage) { create(:ci_stage, status: :skipped) }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_falsy }
end
context 'when stage is manual' do
diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
index 5ab859241c6..b72a818c16c 100644
--- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
+++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
let(:inserter) { instance_double(described_class) }
it 'delegates to bulk insert class' do
- expect(Gitlab::Ci::Tags::BulkInsert)
+ expect(described_class)
.to receive(:new)
.with(statuses)
.and_return(inserter)
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index 6b296924b6d..28c9bdc4c4b 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur
{ key: 'CI_PAGES_DOMAIN',
value: Gitlab.config.pages.host },
{ key: 'CI_PAGES_URL',
- value: project.pages_url },
+ value: Gitlab::Pages::UrlBuilder.new(project).pages_url },
{ key: 'CI_API_V4_URL',
value: API::Helpers::Version.new('v4').root_url },
{ key: 'CI_API_GRAPHQL_URL',
diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
index 432225c53f0..f798945f64f 100644
--- a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
@@ -6,7 +6,7 @@ require 'tsort'
RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
describe '#initialize with non-Collection value' do
- subject { Gitlab::Ci::Variables::Collection::Sort.new([]) }
+ subject { described_class.new([]) }
it 'raises ArgumentError' do
expect { subject }.to raise_error(ArgumentError, /Collection object was expected/)
@@ -167,7 +167,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
- subject { Gitlab::Ci::Variables::Collection::Sort.new(collection).tsort }
+ subject { described_class.new(collection).tsort }
it 'raises TSort::Cyclic' do
expect { subject }.to raise_error(TSort::Cyclic)
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 181e37de9b9..d21190ae297 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -3,6 +3,62 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :secrets_management do
+ describe '.fabricate' do
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "given an array of variables": {
+ input: [
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' }
+ ]
+ },
+ "given a hash of variables": {
+ input: { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ },
+ "given a proc that evaluates to an array": {
+ input: -> do
+ [
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' }
+ ]
+ end
+ },
+ "given a proc that evaluates to a hash": {
+ input: -> do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+ },
+ "given a collection": {
+ input: Gitlab::Ci::Variables::Collection.new(
+ [
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' }
+ ]
+ )
+ }
+ }
+ end
+
+ with_them do
+ subject(:collection) { Gitlab::Ci::Variables::Collection.fabricate(input) }
+
+ it 'returns a collection' do
+ expect(collection).to be_a(Gitlab::Ci::Variables::Collection)
+ expect(collection.size).to eq(2)
+ expect(collection.map(&:key)).to contain_exactly('VAR1', 'VAR2')
+ expect(collection.map(&:value)).to contain_exactly('value1', 'value2')
+ end
+ end
+
+ context 'when given an unrecognized type' do
+ it 'raises error' do
+ expect { described_class.fabricate(1) }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
describe '.new' do
it 'can be initialized with an array' do
variable = { key: 'VAR', value: 'value', public: true, masked: false }
@@ -123,7 +179,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :secrets_man
end
describe '#[]' do
- subject { Gitlab::Ci::Variables::Collection.new(variables)[var_name] }
+ subject { described_class.new(variables)[var_name] }
shared_examples 'an array access operator' do
context 'for a non-existent variable name' do
@@ -570,7 +626,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :secrets_man
end
let(:errors) { 'circular variable reference detected' }
- let(:collection) { Gitlab::Ci::Variables::Collection.new(variables, errors) }
+ let(:collection) { described_class.new(variables, errors) }
subject(:result) { collection.to_s }
diff --git a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb
new file mode 100644
index 00000000000..5b33527e06c
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Downstream::ExpandableVariableGenerator, feature_category: :secrets_management do
+ let(:all_bridge_variables) do
+ Gitlab::Ci::Variables::Collection.fabricate(
+ [
+ { key: 'REF1', value: 'ref 1' },
+ { key: 'REF2', value: 'ref 2' }
+ ]
+ )
+ end
+
+ let(:context) do
+ Gitlab::Ci::Variables::Downstream::Generator::Context.new(all_bridge_variables: all_bridge_variables)
+ end
+
+ subject(:generator) { described_class.new(context) }
+
+ describe '#for' do
+ context 'when given a variable without interpolation' do
+ it 'returns an array containing the variable' do
+ var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: 'variable 1' })
+
+ expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'variable 1' }])
+ end
+ end
+
+ context 'when given a variable with interpolation' do
+ it 'returns an array containing the expanded variables' do
+ var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1 $REF2 $REF3' })
+
+ expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 ' }])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb
new file mode 100644
index 00000000000..61e8b9a8c4a
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: :secrets_management do
+ let(:bridge_variables) do
+ Gitlab::Ci::Variables::Collection.fabricate(
+ [
+ { key: 'REF1', value: 'ref 1' },
+ { key: 'REF2', value: 'ref 2' }
+ ]
+ )
+ end
+
+ let(:yaml_variables) do
+ [
+ { key: 'VAR1', value: 'variable 1' },
+ { key: 'VAR2', value: 'variable 2' },
+ { key: 'RAW_VAR3', value: '$REF1', raw: true },
+ { key: 'INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' }
+ ]
+ end
+
+ let(:pipeline_variables) do
+ [
+ { key: 'PIPELINE_VAR1', value: 'variable 1' },
+ { key: 'PIPELINE_VAR2', value: 'variable 2' },
+ { key: 'PIPELINE_RAW_VAR3', value: '$REF1', raw: true },
+ { key: 'PIPELINE_INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' }
+ ]
+ end
+
+ let(:pipeline_schedule_variables) do
+ [
+ { key: 'PIPELINE_SCHEDULE_VAR1', value: 'variable 1' },
+ { key: 'PIPELINE_SCHEDULE_VAR2', value: 'variable 2' },
+ { key: 'PIPELINE_SCHEDULE_RAW_VAR3', value: '$REF1', raw: true },
+ { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' }
+ ]
+ end
+
+ let(:bridge) do
+ instance_double(
+ 'Ci::Bridge',
+ variables: bridge_variables,
+ forward_yaml_variables?: true,
+ forward_pipeline_variables?: true,
+ yaml_variables: yaml_variables,
+ pipeline_variables: pipeline_variables,
+ pipeline_schedule_variables: pipeline_schedule_variables
+ )
+ end
+
+ subject(:generator) { described_class.new(bridge) }
+
+ describe '#calculate' do
+ it 'creates attributes for downstream pipeline variables from the ' \
+ 'given yaml variables, pipeline variables and pipeline schedule variables' do
+ expected = [
+ { key: 'VAR1', value: 'variable 1' },
+ { key: 'VAR2', value: 'variable 2' },
+ { key: 'RAW_VAR3', value: '$REF1', raw: true },
+ { key: 'INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' },
+ { key: 'PIPELINE_VAR1', value: 'variable 1' },
+ { key: 'PIPELINE_VAR2', value: 'variable 2' },
+ { key: 'PIPELINE_RAW_VAR3', value: '$REF1', raw: true },
+ { key: 'PIPELINE_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' },
+ { key: 'PIPELINE_SCHEDULE_VAR1', value: 'variable 1' },
+ { key: 'PIPELINE_SCHEDULE_VAR2', value: 'variable 2' },
+ { key: 'PIPELINE_SCHEDULE_RAW_VAR3', value: '$REF1', raw: true },
+ { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' }
+ ]
+
+ expect(generator.calculate).to contain_exactly(*expected)
+ end
+
+ it 'returns empty array when bridge has no variables' do
+ allow(bridge).to receive(:yaml_variables).and_return([])
+ allow(bridge).to receive(:pipeline_variables).and_return([])
+ allow(bridge).to receive(:pipeline_schedule_variables).and_return([])
+
+ expect(generator.calculate).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb
new file mode 100644
index 00000000000..12249071486
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Downstream::RawVariableGenerator, feature_category: :secrets_management do
+ let(:context) { Gitlab::Ci::Variables::Downstream::Generator::Context.new }
+
+ subject(:generator) { described_class.new(context) }
+
+ describe '#for' do
+ it 'returns an array containing the unexpanded raw variable' do
+ var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1', raw: true })
+
+ expect(generator.for(var)).to match_array([{ key: 'VAR1', value: '$REF1', raw: true }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 2c020e76cb6..c4e27d0e420 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -152,7 +152,7 @@ module Gitlab
config = YAML.dump({ default: { interruptible: true },
rspec: { script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
builds = config_processor.builds.select { |b| b[:stage] == "test" }
expect(builds.size).to eq(1)
@@ -851,7 +851,7 @@ module Gitlab
context 'when `only` has an invalid value' do
let(:config) { { rspec: { script: "rspec", stage: "test", only: only } } }
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
context 'when it is integer' do
let(:only) { 1 }
@@ -875,7 +875,7 @@ module Gitlab
context 'when `except` has an invalid value' do
let(:config) { { rspec: { script: "rspec", except: except } } }
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
context 'when it is integer' do
let(:except) { 1 }
@@ -899,7 +899,7 @@ module Gitlab
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data).execute }
+ let(:config_processor) { described_class.new(config_data).execute }
subject(:test_build) { config_processor.builds.find { |build| build[:name] == 'test' } }
@@ -1131,7 +1131,7 @@ module Gitlab
before_script: ["pwd"],
rspec: { script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build).to eq({
@@ -1165,7 +1165,7 @@ module Gitlab
command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build).to eq({
@@ -1197,7 +1197,7 @@ module Gitlab
before_script: ["pwd"],
rspec: { script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build).to eq({
@@ -1225,7 +1225,7 @@ module Gitlab
before_script: ["pwd"],
rspec: { image: "image:1.0", services: ["postgresql", "docker:dind"], script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build).to eq({
@@ -1492,7 +1492,7 @@ module Gitlab
end
context 'when using `extends`' do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute }
+ let(:config_processor) { described_class.new(config).execute }
subject { config_processor.builds.first }
@@ -1612,7 +1612,7 @@ module Gitlab
}
end
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts).execute }
+ subject { described_class.new(YAML.dump(config), opts).execute }
context "when validating a ci config file with no project context" do
context "when a single string is provided" do
@@ -1744,7 +1744,7 @@ module Gitlab
variables: { 'VAR1' => 1 } })
end
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute }
+ let(:config_processor) { described_class.new(config).execute }
let(:builds) { config_processor.builds }
context 'when job is parallelized' do
@@ -1860,7 +1860,7 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build[:cache]).to eq(
@@ -1886,7 +1886,7 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build[:cache]).to eq(
@@ -1913,7 +1913,7 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build[:cache]).to eq(
@@ -1952,7 +1952,7 @@ module Gitlab
}
)
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build[:cache]).to eq(
@@ -1979,7 +1979,7 @@ module Gitlab
}
)
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build[:cache]).to eq(
@@ -2004,7 +2004,7 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build[:cache]).to eq(
@@ -2039,7 +2039,7 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' }
expect(rspec_build).to eq({
@@ -2076,7 +2076,7 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
+ config_processor = described_class.new(config).execute
builds = config_processor.builds
expect(builds.size).to eq(1)
@@ -2133,7 +2133,7 @@ module Gitlab
end
describe "release" do
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ let(:processor) { described_class.new(YAML.dump(config)).execute }
let(:config) do
{
stages: %w[build test release],
@@ -2179,7 +2179,7 @@ module Gitlab
}
end
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
let(:builds) { subject.builds }
@@ -2289,7 +2289,7 @@ module Gitlab
}
end
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
let(:builds) { subject.builds }
@@ -2331,7 +2331,7 @@ module Gitlab
}
end
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
context 'no dependencies' do
let(:dependencies) {}
@@ -2404,7 +2404,7 @@ module Gitlab
}
end
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
context 'no needs' do
it { is_expected.to be_valid }
@@ -2678,7 +2678,7 @@ module Gitlab
end
context 'with when/rules' do
- subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute }
+ subject { described_class.new(YAML.dump(config)).execute }
let(:config) do
{
@@ -2798,7 +2798,7 @@ module Gitlab
end
describe "Hidden jobs" do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute }
+ let(:config_processor) { described_class.new(config).execute }
subject { config_processor.builds }
@@ -2846,7 +2846,7 @@ module Gitlab
end
describe "YAML Alias/Anchor" do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute }
+ let(:config_processor) { described_class.new(config).execute }
subject { config_processor.builds }
@@ -3390,7 +3390,7 @@ module Gitlab
end
describe '#execute' do
- subject { Gitlab::Ci::YamlProcessor.new(content).execute }
+ subject { described_class.new(content).execute }
context 'when the YAML could not be parsed' do
let(:content) { YAML.dump('invalid: yaml: test') }
@@ -3435,7 +3435,7 @@ module Gitlab
it 'returns errors and empty configuration' do
expect(subject.valid?).to eq(false)
- expect(subject.errors).to eq(['Unknown alias: bad_alias'])
+ expect(subject.errors).to all match(%r{unknown .+ bad_alias}i)
end
end
diff --git a/spec/lib/gitlab/cleanup/remote_uploads_spec.rb b/spec/lib/gitlab/cleanup/remote_uploads_spec.rb
index c59b7f004dd..fddc62a5705 100644
--- a/spec/lib/gitlab/cleanup/remote_uploads_spec.rb
+++ b/spec/lib/gitlab/cleanup/remote_uploads_spec.rb
@@ -72,4 +72,30 @@ RSpec.describe Gitlab::Cleanup::RemoteUploads do
subject
end
end
+
+ context 'when a bucket prefix is configured' do
+ let(:bucket_prefix) { 'test-prefix' }
+ let(:credentials) do
+ {
+ provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ region: "eu-central-1"
+ }
+ end
+
+ let(:config) { { connection: credentials, bucket_prefix: bucket_prefix, remote_directory: 'uploads' } }
+
+ subject { described_class.new.run!(dry_run: false) }
+
+ before do
+ stub_uploads_object_storage(FileUploader, config: config)
+ end
+
+ it 'does not connect to any storage' do
+ expect(::Fog::Storage).not_to receive(:new)
+
+ expect { subject }.to raise_error(/prefixes are not supported/)
+ end
+ end
end
diff --git a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
index 05b67a8a93f..cc329c48c5f 100644
--- a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
+++ b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::Cluster::Mixins::PumaCluster do
loop do
line = process.readline
puts "PUMA_DEBUG: #{line}" if ENV['PUMA_DEBUG']
- break if line =~ output
+ break if line.match?(output)
end
end
end
diff --git a/spec/lib/gitlab/config/entry/composable_array_spec.rb b/spec/lib/gitlab/config/entry/composable_array_spec.rb
index 77766cb3b0a..d8f00476444 100644
--- a/spec/lib/gitlab/config/entry/composable_array_spec.rb
+++ b/spec/lib/gitlab/config/entry/composable_array_spec.rb
@@ -47,13 +47,13 @@ RSpec.describe Gitlab::Config::Entry::ComposableArray, :aggregate_failures do
expect(entry[0].description).to eq('node definition')
expect(entry[0].key).to eq('node')
expect(entry[0].metadata).to eq({})
- expect(entry[0].parent.class).to eq(Gitlab::Config::Entry::ComposableArray)
+ expect(entry[0].parent.class).to eq(described_class)
expect(entry[0].value).to eq(DATABASE_SECRET: 'passw0rd')
expect(entry[1]).to be_a(Gitlab::Config::Entry::Node)
expect(entry[1].description).to eq('node definition')
expect(entry[1].key).to eq('node')
expect(entry[1].metadata).to eq({})
- expect(entry[1].parent.class).to eq(Gitlab::Config::Entry::ComposableArray)
+ expect(entry[1].parent.class).to eq(described_class)
expect(entry[1].value).to eq(API_TOKEN: 'passw0rd2')
end
diff --git a/spec/lib/gitlab/config/entry/composable_hash_spec.rb b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
index 331c9efc741..6ce66314098 100644
--- a/spec/lib/gitlab/config/entry/composable_hash_spec.rb
+++ b/spec/lib/gitlab/config/entry/composable_hash_spec.rb
@@ -48,19 +48,19 @@ RSpec.describe Gitlab::Config::Entry::ComposableHash, :aggregate_failures do
expect(entry[:DATABASE_SECRET].description).to eq('DATABASE_SECRET node definition')
expect(entry[:DATABASE_SECRET].key).to eq(:DATABASE_SECRET)
expect(entry[:DATABASE_SECRET].metadata).to eq(name: :DATABASE_SECRET)
- expect(entry[:DATABASE_SECRET].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
+ expect(entry[:DATABASE_SECRET].parent.class).to eq(described_class)
expect(entry[:DATABASE_SECRET].value).to eq('passw0rd')
expect(entry[:API_TOKEN]).to be_a(Gitlab::Config::Entry::Node)
expect(entry[:API_TOKEN].description).to eq('API_TOKEN node definition')
expect(entry[:API_TOKEN].key).to eq(:API_TOKEN)
expect(entry[:API_TOKEN].metadata).to eq(name: :API_TOKEN)
- expect(entry[:API_TOKEN].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
+ expect(entry[:API_TOKEN].parent.class).to eq(described_class)
expect(entry[:API_TOKEN].value).to eq('passw0rd2')
expect(entry[:ACCEPT_PASSWORD]).to be_a(Gitlab::Config::Entry::Node)
expect(entry[:ACCEPT_PASSWORD].description).to eq('ACCEPT_PASSWORD node definition')
expect(entry[:ACCEPT_PASSWORD].key).to eq(:ACCEPT_PASSWORD)
expect(entry[:ACCEPT_PASSWORD].metadata).to eq(name: :ACCEPT_PASSWORD)
- expect(entry[:ACCEPT_PASSWORD].parent.class).to eq(Gitlab::Config::Entry::ComposableHash)
+ expect(entry[:ACCEPT_PASSWORD].parent.class).to eq(described_class)
expect(entry[:ACCEPT_PASSWORD].value).to eq(false)
end
diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index bba66f33718..ec83fbc67b5 100644
--- a/spec/lib/gitlab/config/loader/yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_composi
it 'raises FormatError' do
expect { loader }.to raise_error(
Gitlab::Config::Loader::FormatError,
- 'Unknown alias: bad_alias'
+ %r{unknown .+ bad_alias}i
)
end
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index fda3b07eb82..0e93a85764f 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -276,7 +276,7 @@ RSpec.describe Gitlab::CurrentSettings do
describe '#current_application_settings?', :use_clean_rails_memory_store_caching do
before do
- allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original
+ allow(described_class).to receive(:current_application_settings?).and_call_original
end
it 'returns true when settings exist' do
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 92fef93bddb..7cd0af0dcec 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -66,25 +66,6 @@ RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do
expect(control.count).to eq(14)
end
- context 'when job_webhook_retries_count feature flag is disabled' do
- before do
- stub_feature_flags(job_webhook_retries_count: false)
- end
-
- it { expect(data).not_to have_key(:retries_count) }
-
- it 'does not exceed number of expected queries' do
- ci_build # Make sure the Ci::Build model is created before recording.
-
- control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- b = Ci::Build.find(ci_build.id)
- described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory
- end
-
- expect(control.count).to eq(13)
- end
- end
-
context 'commit author_url' do
context 'when no commit present' do
let(:build) { build(:ci_build) }
diff --git a/spec/lib/gitlab/data_builder/emoji_spec.rb b/spec/lib/gitlab/data_builder/emoji_spec.rb
new file mode 100644
index 00000000000..a9cd9824e55
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/emoji_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DataBuilder::Emoji, feature_category: :team_planning do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:snippet) { create(:snippet, project: project) }
+ let(:action) { 'award' }
+ let(:data) { described_class.build(award_emoji, user, action) }
+ let(:award_emoji) { create(:award_emoji, awardable: awardable) }
+
+ shared_examples 'includes standard data' do
+ specify do
+ expect(awardable).to receive(:hook_attrs)
+ expect(data[:object_attributes]).to have_key(:awarded_on_url)
+ expect(data[:object_kind]).to eq('emoji')
+ expect(data[:user]).to eq(user.hook_attrs)
+ end
+
+ include_examples 'project hook data'
+ end
+
+ describe 'when emoji on issue' do
+ let(:awardable) { issue }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the issue data' do
+ expect(awardable).to receive(:hook_attrs)
+ expect(data).to have_key(:issue)
+ end
+ end
+
+ describe 'when emoji on merge request' do
+ let(:awardable) { merge_request }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the merge request data' do
+ expect(awardable).to receive(:hook_attrs)
+ expect(data).to have_key(:merge_request)
+ end
+ end
+
+ describe 'when emoji on snippet' do
+ let(:awardable) { snippet }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the snippet data' do
+ expect(awardable).to receive(:hook_attrs)
+ expect(data).to have_key(:snippet)
+ end
+ end
+
+ describe 'when emoji on note' do
+ describe 'when note on issue' do
+ let(:note) { create(:note, noteable: issue, project: project) }
+ let(:awardable) { note }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the note and issue data' do
+ expect(note.noteable).to receive(:hook_attrs)
+ expect(data).to have_key(:note)
+ expect(data).to have_key(:issue)
+ end
+ end
+
+ describe 'when note on merge request' do
+ let(:note) { create(:note, noteable: merge_request, project: project) }
+ let(:awardable) { note }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the note and merge request data' do
+ expect(note.noteable).to receive(:hook_attrs)
+ expect(data).to have_key(:note)
+ expect(data).to have_key(:merge_request)
+ end
+ end
+
+ describe 'when note on snippet' do
+ let(:note) { create(:note, noteable: snippet, project: project) }
+ let(:awardable) { note }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the note and snippet data' do
+ expect(note.noteable).to receive(:hook_attrs)
+ expect(data).to have_key(:note)
+ expect(data).to have_key(:snippet)
+ end
+ end
+
+ describe 'when note on commit' do
+ let(:note) { create(:note_on_commit, project: project) }
+ let(:awardable) { note }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the note and commit data' do
+ expect(note.noteable).to receive(:hook_attrs)
+ expect(data).to have_key(:note)
+ expect(data).to have_key(:commit)
+ end
+ end
+ end
+
+ describe 'when awardable does not respond to hook_attrs' do
+ let(:awardable) { issue }
+
+ it_behaves_like 'includes standard data'
+
+ it 'returns the issue data' do
+ allow(award_emoji.awardable).to receive(:respond_to?).with(:hook_attrs).and_return(false)
+ expect(Gitlab::AppLogger).to receive(:error).with(
+ 'Error building payload data for emoji webhook. Issue does not respond to hook_attrs.')
+
+ data
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
index b2ba1a60fbb..309bbf1e3f0 100644
--- a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
@@ -223,6 +223,21 @@ RSpec.describe Gitlab::Database::AsyncIndexes::MigrationHelpers, feature_categor
expect(async_index).to have_attributes(table_name: table_name, definition: index_definition)
end
end
+
+ context 'when the given SQL has whitespace' do
+ let(:index_definition) { " #{super()}" }
+ let(:async_index) { index_model.find_by(name: index_name) }
+
+ it 'creates the async index record' do
+ expect { prepare_async_index_from_sql }.to change { index_model.where(name: index_name).count }.by(1)
+ end
+
+ it 'sets the async index attributes correctly' do
+ prepare_async_index_from_sql
+
+ expect(async_index).to have_attributes(table_name: table_name, definition: index_definition.strip)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
index 9e37124ba28..7e111dbe08f 100644
--- a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
@@ -55,6 +55,21 @@ RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model,
it_behaves_like 'table_name is invalid'
end
+
+ context 'when passing a definition with beginning or trailing whitespace' do
+ let(:model) { super().tap { |m| m.definition = definition } }
+ let(:definition) do
+ <<-SQL
+ CREATE UNIQUE INDEX CONCURRENTLY foo_index ON bar_field (uuid);
+ SQL
+ end
+
+ it "strips the definition field" do
+ expect(model).to be_valid
+ model.save!
+ expect(model.definition).to eq(definition.strip)
+ end
+ end
end
describe 'scopes' do
diff --git a/spec/lib/gitlab/database/click_house_client_spec.rb b/spec/lib/gitlab/database/click_house_client_spec.rb
new file mode 100644
index 00000000000..502d879bf6a
--- /dev/null
+++ b/spec/lib/gitlab/database/click_house_client_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ClickHouse::Client', feature_category: :database do
+ context 'when click_house spec tag is not added' do
+ it 'does not have any ClickHouse databases configured' do
+ databases = ClickHouse::Client.configuration.databases
+
+ expect(databases).to be_empty
+ end
+ end
+
+ describe 'when click_house spec tag is added', :click_house do
+ around do |example|
+ with_net_connect_allowed do
+ example.run
+ end
+ end
+
+ it 'has a ClickHouse database configured' do
+ databases = ClickHouse::Client.configuration.databases
+
+ expect(databases).not_to be_empty
+ end
+
+ it 'returns data from the DB via `select` method' do
+ result = ClickHouse::Client.select("SELECT 1 AS value", :main)
+
+ # returns JSON if successful. Otherwise error
+ expect(result).to eq([{ 'value' => 1 }])
+ end
+
+ it 'does not return data via `execute` method' do
+ result = ClickHouse::Client.execute("SELECT 1 AS value", :main)
+
+ # does not return data, just true if successful. Otherwise error.
+ expect(result).to eq(true)
+ end
+
+ describe 'data manipulation' do
+ describe 'inserting' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project) }
+
+ let_it_be(:author1) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:author2) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let_it_be(:issue1) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ let_it_be(:event1) { create(:event, :created, target: issue1, author: author1) }
+ let_it_be(:event2) { create(:event, :closed, target: issue2, author: author2) }
+ let_it_be(:event3) { create(:event, :merged, target: merge_request, author: author1) }
+
+ let(:events) { [event1, event2, event3] }
+
+ def format_row(event)
+ path = event.project.reload.project_namespace.traversal_ids.join('/')
+
+ action = Event.actions[event.action]
+ [
+ event.id,
+ "'#{path}/'",
+ event.author_id,
+ event.target_id,
+ "'#{event.target_type}'",
+ action,
+ event.created_at.to_f,
+ event.updated_at.to_f
+ ].join(',')
+ end
+
+ describe 'RSpec hooks' do
+ it 'ensures that tables are empty' do
+ results = ClickHouse::Client.select('SELECT * FROM events', :main)
+ expect(results).to be_empty
+ end
+ end
+
+ it 'inserts and modifies data' do
+ insert_query = <<~SQL
+ INSERT INTO events
+ (id, path, author_id, target_id, target_type, action, created_at, updated_at)
+ VALUES
+ (#{format_row(event1)}),
+ (#{format_row(event2)}),
+ (#{format_row(event3)})
+ SQL
+
+ ClickHouse::Client.execute(insert_query, :main)
+
+ results = ClickHouse::Client.select('SELECT * FROM events ORDER BY id', :main)
+ expect(results.size).to eq(3)
+
+ last = results.last
+ expect(last).to match(a_hash_including(
+ 'id' => event3.id,
+ 'author_id' => event3.author_id,
+ 'created_at' => be_within(0.05).of(event3.created_at),
+ 'target_type' => event3.target_type
+ ))
+
+ ClickHouse::Client.execute("DELETE FROM events WHERE id = #{event3.id}", :main)
+
+ results = ClickHouse::Client.select("SELECT * FROM events WHERE id = #{event3.id}", :main)
+ expect(results).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/each_database_spec.rb b/spec/lib/gitlab/database/each_database_spec.rb
index 2653297c81a..d0e3c83076c 100644
--- a/spec/lib/gitlab/database/each_database_spec.rb
+++ b/spec/lib/gitlab/database/each_database_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::EachDatabase do
- describe '.each_database_connection', :add_ci_connection do
+ describe '.each_connection', :add_ci_connection do
let(:database_base_models) { { main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access }
before do
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
.with(Ci::ApplicationRecord.connection).ordered.and_yield
- expect { |b| described_class.each_database_connection(&b) }
+ expect { |b| described_class.each_connection(&b) }
.to yield_successive_args(
[ActiveRecord::Base.connection, 'main'],
[Ci::ApplicationRecord.connection, 'ci']
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
.with(Ci::ApplicationRecord.connection).ordered.and_yield
- expect { |b| described_class.each_database_connection(only: 'ci', &b) }
+ expect { |b| described_class.each_connection(only: 'ci', &b) }
.to yield_successive_args([Ci::ApplicationRecord.connection, 'ci'])
end
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
.with(Ci::ApplicationRecord.connection).ordered.and_yield
- expect { |b| described_class.each_database_connection(only: :ci, &b) }
+ expect { |b| described_class.each_connection(only: :ci, &b) }
.to yield_successive_args([Ci::ApplicationRecord.connection, 'ci'])
end
end
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
context 'when the selected names are invalid' do
it 'does not yield any connections' do
expect do |b|
- described_class.each_database_connection(only: :notvalid, &b)
+ described_class.each_connection(only: :notvalid, &b)
rescue ArgumentError => e
expect(e.message).to match(/notvalid is not a valid database name/)
end.not_to yield_control
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
it 'raises an error' do
expect do
- described_class.each_database_connection(only: :notvalid) {}
+ described_class.each_connection(only: :notvalid) {}
end.to raise_error(ArgumentError, /notvalid is not a valid database name/)
end
end
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::Database::EachDatabase do
db_config.name != 'main' ? 'main' : nil
end
- expect { |b| described_class.each_database_connection(include_shared: false, &b) }
+ expect { |b| described_class.each_connection(include_shared: false, &b) }
.to yield_successive_args([ActiveRecord::Base.connection, 'main'])
end
end
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
index 48f5cdb995b..1c864239ae6 100644
--- a/spec/lib/gitlab/database/gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -29,6 +29,9 @@ RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do
'audit_events_part_5fc467ac26' | :gitlab_main
'_test_gitlab_main_table' | :gitlab_main
'_test_gitlab_ci_table' | :gitlab_ci
+ '_test_gitlab_main_clusterwide_table' | :gitlab_main_clusterwide
+ '_test_gitlab_main_cell_table' | :gitlab_main_cell
+ '_test_gitlab_pm_table' | :gitlab_pm
'_test_my_table' | :gitlab_shared
'pg_attribute' | :gitlab_internal
end
diff --git a/spec/lib/gitlab/database/health_status_spec.rb b/spec/lib/gitlab/database/health_status_spec.rb
index bc923635b1d..4a2b9eee45a 100644
--- a/spec/lib/gitlab/database/health_status_spec.rb
+++ b/spec/lib/gitlab/database/health_status_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::Database::HealthStatus, feature_category: :database do
end
let(:deferred_worker_health_checker) do
- Gitlab::SidekiqMiddleware::DeferJobs::DatabaseHealthStatusChecker.new(
+ Gitlab::SidekiqMiddleware::SkipJobs::DatabaseHealthStatusChecker.new(
123,
deferred_worker.name
)
@@ -116,7 +116,7 @@ RSpec.describe Gitlab::Database::HealthStatus, feature_category: :database do
it 'captures sidekiq job class in the log' do
expect(Gitlab::Database::HealthStatus::Logger).to receive(:info).with(
status_checker_id: deferred_worker_health_checker.id,
- status_checker_type: 'Gitlab::SidekiqMiddleware::DeferJobs::DatabaseHealthStatusChecker',
+ status_checker_type: 'Gitlab::SidekiqMiddleware::SkipJobs::DatabaseHealthStatusChecker',
job_class_name: deferred_worker_health_checker.job_class_name,
health_status_indicator: autovacuum_indicator_class.to_s,
indicator_signal: 'Stop',
diff --git a/spec/lib/gitlab/database/load_balancing/host_spec.rb b/spec/lib/gitlab/database/load_balancing/host_spec.rb
index caae06ce43a..5ef6d9173c4 100644
--- a/spec/lib/gitlab/database/load_balancing/host_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/host_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do
end
let(:host) do
- Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer)
+ described_class.new('localhost', load_balancer)
end
before do
diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
index 997c7a31cba..26c8969efd8 100644
--- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
+RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store, feature_category: :database do
let(:conflict_error) { Class.new(RuntimeError) }
let(:model) { ActiveRecord::Base }
let(:db_host) { model.connection_pool.db_config.host }
@@ -71,7 +71,30 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
end
end
+ shared_examples 'logs service discovery thread interruption' do |lb_method|
+ context 'with service discovery' do
+ let(:service_discovery) do
+ instance_double(
+ Gitlab::Database::LoadBalancing::ServiceDiscovery,
+ log_refresh_thread_interruption: true
+ )
+ end
+
+ before do
+ allow(lb).to receive(:service_discovery).and_return(service_discovery)
+ end
+
+ it 'calls logs service discovery thread interruption' do
+ expect(service_discovery).to receive(:log_refresh_thread_interruption)
+
+ lb.public_send(lb_method) {}
+ end
+ end
+ end
+
describe '#read' do
+ it_behaves_like 'logs service discovery thread interruption', :read
+
it 'yields a connection for a read' do
connection = double(:connection)
host = double(:host)
@@ -203,6 +226,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
end
describe '#read_write' do
+ it_behaves_like 'logs service discovery thread interruption', :read_write
+
it 'yields a connection for a write' do
connection = ActiveRecord::Base.connection_pool.connection
diff --git a/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb b/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb
index 02c9499bedb..53605d14c17 100644
--- a/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
)
end
- let(:host) { Gitlab::Database::LoadBalancing::PrimaryHost.new(load_balancer) }
+ let(:host) { described_class.new(load_balancer) }
describe '#connection' do
it 'returns a connection from the pool' do
diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
index 9a559c7ccb4..789919d2a51 100644
--- a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
@@ -58,14 +58,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego
end
end
- describe '#start' do
+ describe '#start', :freeze_time do
before do
allow(service)
.to receive(:loop)
.and_yield
end
- it 'starts service discovery in a new thread' do
+ it 'starts service discovery in a new thread with proper assignments' do
expect(Thread).to receive(:new).ordered.and_call_original # Thread starts
expect(service).to receive(:perform_service_discovery).ordered.and_return(5)
@@ -73,6 +73,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego
expect(service).to receive(:sleep).ordered.with(7) # Sleep runs after thread starts
service.start.join
+
+ expect(service.refresh_thread_last_run).to eq(Time.current)
+ expect(service.refresh_thread).to be_present
end
end
@@ -142,21 +145,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego
service.perform_service_discovery
end
end
-
- context 'with Exception' do
- it 'logs error and re-raises the exception' do
- error = Exception.new('uncaught-test-error')
-
- expect(service).to receive(:refresh_if_necessary).and_raise(error)
-
- expect(Gitlab::Database::LoadBalancing::Logger).to receive(:error).with(
- event: :service_discovery_unexpected_exception,
- message: "Service discovery encountered an uncaught error: uncaught-test-error"
- )
-
- expect { service.perform_service_discovery }.to raise_error(Exception, error.message)
- end
- end
end
describe '#refresh_if_necessary' do
@@ -427,4 +415,64 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego
end
end
end
+
+ describe '#log_refresh_thread_interruption' do
+ before do
+ service.refresh_thread = refresh_thread
+ service.refresh_thread_last_run = last_run_timestamp
+ end
+
+ let(:refresh_thread) { nil }
+ let(:last_run_timestamp) { nil }
+
+ subject { service.log_refresh_thread_interruption }
+
+ context 'without refresh thread timestamp' do
+ it 'does not log any interruption' do
+ expect(service.refresh_thread_last_run).to be_nil
+
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:error)
+
+ subject
+ end
+ end
+
+ context 'with refresh thread timestamp' do
+ let(:last_run_timestamp) { Time.current }
+
+ it 'does not log if last run time plus delta is in future' do
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:error)
+
+ subject
+ end
+
+ context 'with way past last run timestamp' do
+ let(:refresh_thread) { instance_double(Thread, status: :run, backtrace: %w[backtrace foo]) }
+ let(:last_run_timestamp) { 20.minutes.before + described_class::DISCOVERY_THREAD_REFRESH_DELTA.minutes }
+
+ it 'does not log if the interruption is already logged' do
+ service.refresh_thread_interruption_logged = true
+
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:error)
+
+ subject
+ end
+
+ it 'logs the error if the interruption was not logged before' do
+ expect(service.refresh_thread_interruption_logged).not_to be_present
+
+ expect(Gitlab::Database::LoadBalancing::Logger).to receive(:error).with(
+ event: :service_discovery_refresh_thread_interrupt,
+ refresh_thread_last_run: last_run_timestamp,
+ thread_status: refresh_thread.status.to_s,
+ thread_backtrace: 'backtrace\nfoo'
+ )
+
+ subject
+
+ expect(service.refresh_thread_interruption_logged).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
index e4241348b54..577bf00ba2f 100644
--- a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
@@ -9,7 +9,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
let(:schema_class) { Class.new(Gitlab::Database::Migration[2.1]) }
let(:skip_automatic_lock_on_writes) { false }
let(:gitlab_main_table_name) { :_test_gitlab_main_table }
+ let(:gitlab_main_clusterwide_table_name) { :_test_gitlab_main_clusterwide_table }
+ let(:gitlab_main_cell_table_name) { :_test_gitlab_main_cell_table }
let(:gitlab_ci_table_name) { :_test_gitlab_ci_table }
+ let(:gitlab_pm_table_name) { :_test_gitlab_pm_table }
let(:gitlab_geo_table_name) { :_test_gitlab_geo_table }
let(:gitlab_shared_table_name) { :_test_table }
@@ -24,8 +27,11 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
# Drop the created test tables, because we use non-transactional tests
after do
drop_table_if_exists(gitlab_main_table_name)
+ drop_table_if_exists(gitlab_main_clusterwide_table_name)
+ drop_table_if_exists(gitlab_main_cell_table_name)
drop_table_if_exists(gitlab_ci_table_name)
drop_table_if_exists(gitlab_geo_table_name)
+ drop_table_if_exists(gitlab_pm_table_name)
drop_table_if_exists(gitlab_shared_table_name)
drop_table_if_exists(renamed_gitlab_main_table_name)
drop_table_if_exists(renamed_gitlab_ci_table_name)
@@ -82,8 +88,12 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
context 'when single database' do
let(:config_model) { Gitlab::Database.database_base_models[:main] }
let(:create_gitlab_main_table_migration_class) { create_table_migration(gitlab_main_table_name) }
+ let(:create_gitlab_main_cell_table_migration_class) { create_table_migration(gitlab_main_cell_table_name) }
let(:create_gitlab_ci_table_migration_class) { create_table_migration(gitlab_ci_table_name) }
let(:create_gitlab_shared_table_migration_class) { create_table_migration(gitlab_shared_table_name) }
+ let(:create_gitlab_main_clusterwide_table_migration_class) do
+ create_table_migration(gitlab_main_clusterwide_table_name)
+ end
before do
skip_if_database_exists(:ci)
@@ -93,13 +103,19 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
expect(Gitlab::Database::LockWritesManager).not_to receive(:new)
create_gitlab_main_table_migration_class.migrate(:up)
+ create_gitlab_main_cell_table_migration_class.migrate(:up)
+ create_gitlab_main_clusterwide_table_migration_class.migrate(:up)
create_gitlab_ci_table_migration_class.migrate(:up)
create_gitlab_shared_table_migration_class.migrate(:up)
expect do
create_gitlab_main_table_migration_class.connection.execute("DELETE FROM #{gitlab_main_table_name}")
+ create_gitlab_main_cell_table_migration_class.connection.execute("DELETE FROM #{gitlab_main_cell_table_name}")
create_gitlab_ci_table_migration_class.connection.execute("DELETE FROM #{gitlab_ci_table_name}")
create_gitlab_shared_table_migration_class.connection.execute("DELETE FROM #{gitlab_shared_table_name}")
+ create_gitlab_main_clusterwide_table_migration_class.connection.execute(
+ "DELETE FROM #{gitlab_main_clusterwide_table_name}"
+ )
end.not_to raise_error
end
end
@@ -163,6 +179,27 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
end
end
+ context 'for creating a gitlab_main_clusterwide table' do
+ let(:table_name) { gitlab_main_clusterwide_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'for creating a gitlab_main_cell table' do
+ let(:table_name) { gitlab_main_cell_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'for creating a gitlab_pm table' do
+ let(:table_name) { gitlab_pm_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
context 'for creating a gitlab_ci table' do
let(:table_name) { gitlab_ci_table_name }
diff --git a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
index 6092d985ce8..dcc088c2e21 100644
--- a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
+++ b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
def use_transaction?(migration)
receiver.use_transaction?(migration)
end
- end.prepend(Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigratorLockRetries)
+ end.prepend(described_class)
end
subject { class_def.new(receiver) }
diff --git a/spec/lib/gitlab/database/migrations/redis_helpers_spec.rb b/spec/lib/gitlab/database/migrations/redis_helpers_spec.rb
new file mode 100644
index 00000000000..5f3a48289e2
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/redis_helpers_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::Database::Migrations::RedisHelpers, feature_category: :redis do
+ let(:migration) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ describe "#queue_redis_migration_job" do
+ let(:job_name) { 'SampleJob' }
+
+ subject { migration.queue_redis_migration_job(job_name) }
+
+ context 'when migrator does not exist' do
+ it 'raises error and fails the migration' do
+ expect { subject }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context 'when migrator exists' do
+ before do
+ allow(RedisMigrationWorker).to receive(:fetch_migrator!)
+ end
+
+ it 'checks migrator and enqueues job' do
+ expect(RedisMigrationWorker).to receive(:perform_async).with(job_name, '0')
+
+ subject
+ end
+ end
+ 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 6bcefa455cf..31a194ae883 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
@@ -153,6 +153,25 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
expect(calls.size).to eq(1)
end
+ it 'does not sample a job if there are zero rows to sample' do
+ calls = []
+ define_background_migration(migration_name, with_base_class: true, scoping: ->(relation) {
+ relation.none
+ }) do |*args|
+ 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.count).to eq(0)
+ 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_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb
index 8724716dd3d..a1ae75ac916 100644
--- a/spec/lib/gitlab/database/partitioning_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_spec.rb
@@ -257,7 +257,7 @@ RSpec.describe Gitlab::Database::Partitioning, feature_category: :database do
end
it 'drops detached partitions for each database' do
- expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection).and_yield
+ expect(Gitlab::Database::EachDatabase).to receive(:each_connection).and_yield
expect { described_class.drop_detached_partitions }
.to change { Postgresql::DetachedPartition.count }.from(2).to(0)
diff --git a/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb
deleted file mode 100644
index 6e1e53e0e41..00000000000
--- a/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::PostgresqlAdapter::EmptyQueryPing do
- describe '#active?' do
- let(:adapter_class) do
- Class.new do
- include Gitlab::Database::PostgresqlAdapter::EmptyQueryPing
-
- def initialize(connection, lock)
- @connection = connection
- @lock = lock
- end
- end
- end
-
- subject { adapter_class.new(connection, lock).active? }
-
- let(:connection) { double(query: nil) }
- let(:lock) { double }
-
- before do
- allow(lock).to receive(:synchronize).and_yield
- end
-
- it 'uses an empty query to check liveness' do
- expect(connection).to receive(:query).with(';')
-
- subject
- end
-
- it 'returns true if no error was signaled' do
- expect(subject).to be_truthy
- end
-
- it 'returns false when an error occurs' do
- expect(lock).to receive(:synchronize).and_raise(PG::Error)
-
- expect(subject).to be_falsey
- end
- end
-end
diff --git a/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
index c6542aa2adb..75c3a3650d7 100644
--- a/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
+++ b/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
describe '#initialize_type_map' do
it 'caches loading of types in memory' do
recorder_without_cache = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection.disconnect! }
- expect(recorder_without_cache.log).to include(a_string_matching(/FROM pg_type/)).twice
+ expect(recorder_without_cache.log).to include(a_string_matching(/FROM pg_type/)).exactly(4).times
recorder_with_cache = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection.disconnect! }
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
recorder = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection(other_config).disconnect! }
- expect(recorder.log).to include(a_string_matching(/FROM pg_type/)).twice
+ expect(recorder.log).to include(a_string_matching(/FROM pg_type/)).exactly(4).times
end
end
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
connection = initialize_connection
recorder = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { connection.reload_type_map }
- expect(recorder.log).to include(a_string_matching(/FROM pg_type/)).once
+ expect(recorder.log).to include(a_string_matching(/FROM pg_type/)).exactly(3).times
end
end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 4d0e58b0937..851fc7ea3cd 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -99,14 +99,14 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
end
before do
- allow(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!)
- allow(Gitlab::Database::Reindexing).to receive(:perform_from_queue).and_return(0)
- allow(Gitlab::Database::Reindexing).to receive(:perform_with_heuristic).and_return(0)
+ allow(described_class).to receive(:cleanup_leftovers!)
+ allow(described_class).to receive(:perform_from_queue).and_return(0)
+ allow(described_class).to receive(:perform_with_heuristic).and_return(0)
end
it 'cleans up leftovers, before consuming the queue' do
- expect(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!).ordered
- expect(Gitlab::Database::Reindexing).to receive(:perform_from_queue).ordered
+ expect(described_class).to receive(:cleanup_leftovers!).ordered
+ expect(described_class).to receive(:perform_from_queue).ordered
subject
end
@@ -120,8 +120,8 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
let(:limit) { 1 }
it 'does not perform reindexing with heuristic' do
- expect(Gitlab::Database::Reindexing).to receive(:perform_from_queue).and_return(limit)
- expect(Gitlab::Database::Reindexing).not_to receive(:perform_with_heuristic)
+ expect(described_class).to receive(:perform_from_queue).and_return(limit)
+ expect(described_class).not_to receive(:perform_with_heuristic)
subject
end
@@ -131,8 +131,8 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
let(:limit) { 2 }
it 'continues if the queue did not have enough records' do
- expect(Gitlab::Database::Reindexing).to receive(:perform_from_queue).ordered.and_return(1)
- expect(Gitlab::Database::Reindexing).to receive(:perform_with_heuristic).with(maximum_records: 1).ordered
+ expect(described_class).to receive(:perform_from_queue).ordered.and_return(1)
+ expect(described_class).to receive(:perform_with_heuristic).with(maximum_records: 1).ordered
subject
end
diff --git a/spec/lib/gitlab/database/schema_validation/adapters/column_database_adapter_spec.rb b/spec/lib/gitlab/database/schema_validation/adapters/column_database_adapter_spec.rb
deleted file mode 100644
index d81f5f3dbec..00000000000
--- a/spec/lib/gitlab/database/schema_validation/adapters/column_database_adapter_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Adapters::ColumnDatabaseAdapter, feature_category: :database do
- subject(:adapter) { described_class.new(db_result) }
-
- let(:column_name) { 'email' }
- let(:column_default) { "'no-reply@gitlab.com'::character varying" }
- let(:not_null) { true }
- let(:partition_key) { false }
- let(:db_result) do
- {
- 'table_name' => 'projects',
- 'column_name' => column_name,
- 'data_type' => 'character varying',
- 'column_default' => column_default,
- 'not_null' => not_null,
- 'partition_key' => partition_key
- }
- end
-
- describe '#name' do
- it { expect(adapter.name).to eq('email') }
- end
-
- describe '#table_name' do
- it { expect(adapter.table_name).to eq('projects') }
- end
-
- describe '#data_type' do
- it { expect(adapter.data_type).to eq('character varying') }
- end
-
- describe '#default' do
- context "when there's no default value in the column" do
- let(:column_default) { nil }
-
- it { expect(adapter.default).to be_nil }
- end
-
- context 'when the column name is id' do
- let(:column_name) { 'id' }
-
- it { expect(adapter.default).to be_nil }
- end
-
- context 'when the column default includes nextval' do
- let(:column_default) { "nextval('my_seq'::regclass)" }
-
- it { expect(adapter.default).to be_nil }
- end
-
- it { expect(adapter.default).to eq("DEFAULT 'no-reply@gitlab.com'::character varying") }
- end
-
- describe '#nullable' do
- context 'when column is not null' do
- it { expect(adapter.nullable).to eq('NOT NULL') }
- end
-
- context 'when column is nullable' do
- let(:not_null) { false }
-
- it { expect(adapter.nullable).to be_nil }
- end
- end
-
- describe '#partition_key?' do
- it { expect(adapter.partition_key?).to be(false) }
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter_spec.rb b/spec/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter_spec.rb
deleted file mode 100644
index 64b59e65be6..00000000000
--- a/spec/lib/gitlab/database/schema_validation/adapters/column_structure_sql_adapter_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Adapters::ColumnStructureSqlAdapter, feature_category: :database do
- subject(:adapter) { described_class.new(table_name, column_def, partition_stmt) }
-
- let(:table_name) { 'test_table' }
- let(:file_path) { Rails.root.join('spec/fixtures/structure.sql') }
- let(:table_stmts) { PgQuery.parse(File.read(file_path)).tree.stmts.filter_map { |s| s.stmt.create_stmt } }
- let(:table) { table_stmts.find { |table| table.relation.relname == table_name } }
- let(:partition_stmt) { table.partspec }
- let(:column_stmts) { table.table_elts }
- let(:column_def) { column_stmts.find { |col| col.column_def.colname == column_name }.column_def }
-
- where(:column_name, :data_type, :default_value, :nullable, :partition_key) do
- [
- ['id', 'bigint', nil, 'NOT NULL', false],
- ['integer_column', 'integer', nil, nil, false],
- ['integer_with_default_column', 'integer', 'DEFAULT 1', nil, false],
- ['smallint_with_default_column', 'smallint', 'DEFAULT 0', 'NOT NULL', false],
- ['double_precision_with_default_column', 'double precision', 'DEFAULT 1.0', nil, false],
- ['numeric_with_default_column', 'numeric', 'DEFAULT 1.0', 'NOT NULL', false],
- ['boolean_with_default_colum', 'boolean', 'DEFAULT true', 'NOT NULL', false],
- ['varying_with_default_column', 'character varying', "DEFAULT 'DEFAULT'::character varying", 'NOT NULL', false],
- ['varying_with_limit_and_default_column', 'character varying(255)', "DEFAULT 'DEFAULT'::character varying",
- nil, false],
- ['text_with_default_column', 'text', "DEFAULT ''::text", 'NOT NULL', false],
- ['array_with_default_column', 'character varying(255)[]', "DEFAULT '{one,two}'::character varying[]",
- 'NOT NULL', false],
- ['jsonb_with_default_column', 'jsonb', "DEFAULT '[]'::jsonb", 'NOT NULL', false],
- ['timestamptz_with_default_column', 'timestamp(6) with time zone', "DEFAULT now()", nil, false],
- ['timestamp_with_default_column', 'timestamp(6) without time zone',
- "DEFAULT '2022-01-23 00:00:00+00'::timestamp without time zone", 'NOT NULL', false],
- ['date_with_default_column', 'date', 'DEFAULT 2023-04-05', nil, false],
- ['inet_with_default_column', 'inet', "DEFAULT '0.0.0.0'::inet", 'NOT NULL', false],
- ['macaddr_with_default_column', 'macaddr', "DEFAULT '00-00-00-00-00-000'::macaddr", 'NOT NULL', false],
- ['uuid_with_default_column', 'uuid', "DEFAULT '00000000-0000-0000-0000-000000000000'::uuid", 'NOT NULL', false],
- ['partition_key', 'bigint', 'DEFAULT 1', 'NOT NULL', true],
- ['created_at', 'timestamp with time zone', 'DEFAULT now()', 'NOT NULL', true]
- ]
- end
-
- with_them do
- describe '#name' do
- it { expect(adapter.name).to eq(column_name) }
- end
-
- describe '#table_name' do
- it { expect(adapter.table_name).to eq(table_name) }
- end
-
- describe '#data_type' do
- it { expect(adapter.data_type).to eq(data_type) }
- end
-
- describe '#nullable' do
- it { expect(adapter.nullable).to eq(nullable) }
- end
-
- describe '#default' do
- it { expect(adapter.default).to eq(default_value) }
- end
-
- describe '#partition_key?' do
- it { expect(adapter.partition_key?).to eq(partition_key) }
- end
- end
-
- context 'when the data type is not mapped' do
- let(:column_name) { 'unmapped_column_type' }
- let(:error_class) { Gitlab::Database::SchemaValidation::Adapters::UndefinedPGType }
-
- describe '#data_type' do
- it { expect { adapter.data_type }.to raise_error(error_class) }
- end
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter_spec.rb b/spec/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter_spec.rb
deleted file mode 100644
index cfe5572fb51..00000000000
--- a/spec/lib/gitlab/database/schema_validation/adapters/foreign_key_database_adapter_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Adapters::ForeignKeyDatabaseAdapter, feature_category: :database do
- subject(:adapter) { described_class.new(query_result) }
-
- let(:query_result) do
- {
- 'schema' => 'public',
- 'foreign_key_name' => 'fk_2e88fb7ce9',
- 'table_name' => 'members',
- 'foreign_key_definition' => 'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE'
- }
- end
-
- describe '#name' do
- it { expect(adapter.name).to eq('public.fk_2e88fb7ce9') }
- end
-
- describe '#table_name' do
- it { expect(adapter.table_name).to eq('members') }
- end
-
- describe '#statement' do
- it { expect(adapter.statement).to eq('FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE') }
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter_spec.rb b/spec/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter_spec.rb
deleted file mode 100644
index f7ae0c0f892..00000000000
--- a/spec/lib/gitlab/database/schema_validation/adapters/foreign_key_structure_sql_adapter_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Adapters::ForeignKeyStructureSqlAdapter, feature_category: :database do
- subject(:adapter) { described_class.new(stmt) }
-
- let(:stmt) { PgQuery.parse(sql).tree.stmts.first.stmt.alter_table_stmt }
-
- where(:sql, :name, :table_name, :statement) do
- [
- [
- 'ALTER TABLE ONLY public.issues ADD CONSTRAINT fk_05f1e72feb FOREIGN KEY (author_id) REFERENCES users (id) ' \
- 'ON DELETE SET NULL',
- 'public.fk_05f1e72feb',
- 'issues',
- 'FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL'
- ],
- [
- 'ALTER TABLE public.import_failures ADD CONSTRAINT fk_9a9b9ba21c FOREIGN KEY (user_id) REFERENCES users(id) ' \
- 'ON DELETE CASCADE',
- 'public.fk_9a9b9ba21c',
- 'import_failures',
- 'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE'
- ]
- ]
- end
-
- with_them do
- describe '#name' do
- it { expect(adapter.name).to eq(name) }
- end
-
- describe '#table_name' do
- it { expect(adapter.table_name).to eq(table_name) }
- end
-
- describe '#statement' do
- it { expect(adapter.statement).to eq(statement) }
- end
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/database_spec.rb b/spec/lib/gitlab/database/schema_validation/database_spec.rb
deleted file mode 100644
index 0b5f433b1c9..00000000000
--- a/spec/lib/gitlab/database/schema_validation/database_spec.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'database schema assertions for' do |fetch_by_name_method, exists_method, all_objects_method|
- subject(:database) { described_class.new(connection) }
-
- let(:database_model) { Gitlab::Database.database_base_models['main'] }
- let(:connection) { database_model.connection }
-
- before do
- allow(connection).to receive(:select_rows).and_return(results)
- allow(connection).to receive(:exec_query).and_return(results)
- end
-
- describe "##{fetch_by_name_method}" do
- it 'returns nil when schema object does not exists' do
- expect(database.public_send(fetch_by_name_method, 'invalid-object-name')).to be_nil
- end
-
- it 'returns the schema object by name' do
- expect(database.public_send(fetch_by_name_method, valid_schema_object_name).name).to eq(valid_schema_object_name)
- end
- end
-
- describe "##{exists_method}" do
- it 'returns true when schema object exists' do
- expect(database.public_send(exists_method, valid_schema_object_name)).to be_truthy
- end
-
- it 'returns false when schema object does not exists' do
- expect(database.public_send(exists_method, 'invalid-object')).to be_falsey
- end
- end
-
- describe "##{all_objects_method}" do
- it 'returns all the schema objects' do
- schema_objects = database.public_send(all_objects_method)
-
- expect(schema_objects).to all(be_a(schema_object))
- expect(schema_objects.map(&:name)).to eq([valid_schema_object_name])
- end
- end
-end
-
-RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do
- context 'when having indexes' do
- let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index }
- let(:valid_schema_object_name) { 'index' }
- let(:results) do
- [['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
- end
-
- include_examples 'database schema assertions for', 'fetch_index_by_name', 'index_exists?', 'indexes'
- end
-
- context 'when having triggers' do
- let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Trigger }
- let(:valid_schema_object_name) { 'my_trigger' }
- let(:results) do
- [['my_trigger', 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()']]
- end
-
- include_examples 'database schema assertions for', 'fetch_trigger_by_name', 'trigger_exists?', 'triggers'
- end
-
- context 'when having tables' do
- let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Table }
- let(:valid_schema_object_name) { 'my_table' }
- let(:results) do
- [
- {
- 'table_name' => 'my_table',
- 'column_name' => 'id',
- 'not_null' => true,
- 'data_type' => 'bigint',
- 'partition_key' => false,
- 'column_default' => "nextval('audit_events_id_seq'::regclass)"
- },
- {
- 'table_name' => 'my_table',
- 'column_name' => 'details',
- 'not_null' => false,
- 'data_type' => 'text',
- 'partition_key' => false,
- 'column_default' => nil
- }
- ]
- end
-
- include_examples 'database schema assertions for', 'fetch_table_by_name', 'table_exists?', 'tables'
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/inconsistency_spec.rb b/spec/lib/gitlab/database/schema_validation/inconsistency_spec.rb
deleted file mode 100644
index a49ff8339a1..00000000000
--- a/spec/lib/gitlab/database/schema_validation/inconsistency_spec.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Inconsistency, feature_category: :database do
- let(:validator) { Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes }
-
- let(:database_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
- let(:structure_sql_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (id)' }
-
- let(:structure_stmt) { PgQuery.parse(structure_sql_statement).tree.stmts.first.stmt.index_stmt }
- let(:database_stmt) { PgQuery.parse(database_statement).tree.stmts.first.stmt.index_stmt }
-
- let(:structure_sql_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index.new(structure_stmt) }
- let(:database_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index.new(database_stmt) }
-
- subject(:inconsistency) { described_class.new(validator, structure_sql_object, database_object) }
-
- describe '#object_name' do
- it 'returns the index name' do
- expect(inconsistency.object_name).to eq('index_name')
- end
- end
-
- describe '#diff' do
- it 'returns a diff between the structure.sql and the database' do
- expect(inconsistency.diff).to be_a(Diffy::Diff)
- expect(inconsistency.diff.string1).to eq("#{structure_sql_statement}\n")
- expect(inconsistency.diff.string2).to eq("#{database_statement}\n")
- end
- end
-
- describe '#error_message' do
- it 'returns the error message' do
- stub_const "#{validator}::ERROR_MESSAGE", 'error message %s'
-
- expect(inconsistency.error_message).to eq('error message index_name')
- end
- end
-
- describe '#type' do
- it 'returns the type of the validator' do
- expect(inconsistency.type).to eq('different_definition_indexes')
- end
- end
-
- describe '#table_name' do
- it 'returns the table name' do
- expect(inconsistency.table_name).to eq('achievements')
- end
- end
-
- describe '#object_type' do
- it 'returns the structure sql object type' do
- expect(inconsistency.object_type).to eq('Index')
- end
-
- context 'when the structure sql object is not available' do
- subject(:inconsistency) { described_class.new(validator, nil, database_object) }
-
- it 'returns the database object type' do
- expect(inconsistency.object_type).to eq('Index')
- end
- end
- end
-
- describe '#structure_sql_statement' do
- it 'returns structure sql statement' do
- expect(inconsistency.structure_sql_statement).to eq("#{structure_sql_statement}\n")
- end
- end
-
- describe '#database_statement' do
- it 'returns database statement' do
- expect(inconsistency.database_statement).to eq("#{database_statement}\n")
- end
- end
-
- describe '#inspect' do
- let(:expected_output) do
- <<~MSG
- ------------------------------------------------------
- The index_name index has a different statement between structure.sql and database
- Diff:
- \e[31m-CREATE INDEX index_name ON public.achievements USING btree (id)\e[0m
- \e[32m+CREATE INDEX index_name ON public.achievements USING btree (namespace_id)\e[0m
-
- ------------------------------------------------------
- MSG
- end
-
- it 'prints the inconsistency message' do
- expect(inconsistency.inspect).to eql(expected_output)
- 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
deleted file mode 100644
index f5d1c6ba31b..00000000000
--- a/spec/lib/gitlab/database/schema_validation/runner_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# 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::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/column_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/column_spec.rb
deleted file mode 100644
index 74bc5f43b50..00000000000
--- a/spec/lib/gitlab/database/schema_validation/schema_objects/column_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Column, feature_category: :database do
- subject(:column) { described_class.new(adapter) }
-
- let(:database_adapter) { 'Gitlab::Database::SchemaValidation::Adapters::ColumnDatabaseAdapter' }
- let(:adapter) do
- instance_double(database_adapter, name: 'id', table_name: 'projects',
- data_type: 'bigint', default: nil, nullable: 'NOT NULL')
- end
-
- describe '#name' do
- it { expect(column.name).to eq('id') }
- end
-
- describe '#table_name' do
- it { expect(column.table_name).to eq('projects') }
- end
-
- describe '#statement' do
- it { expect(column.statement).to eq('id bigint NOT NULL') }
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/schema_objects/foreign_key_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/foreign_key_spec.rb
deleted file mode 100644
index 7500ad44f82..00000000000
--- a/spec/lib/gitlab/database/schema_validation/schema_objects/foreign_key_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::ForeignKey, feature_category: :database do
- subject(:foreign_key) { described_class.new(adapter) }
-
- let(:database_adapter) { 'Gitlab::Database::SchemaValidation::Adapters::ForeignKeyDatabaseAdapter' }
- let(:adapter) do
- instance_double(database_adapter, name: 'public.fk_1d37cddf91', table_name: 'vulnerabilities',
- statement: 'FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL')
- end
-
- describe '#name' do
- it { expect(foreign_key.name).to eq('public.fk_1d37cddf91') }
- end
-
- describe '#table_name' do
- it { expect(foreign_key.table_name).to eq('vulnerabilities') }
- end
-
- describe '#statement' do
- it { expect(foreign_key.statement).to eq('FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL') }
- 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
deleted file mode 100644
index 43d8fa38ec8..00000000000
--- a/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# 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' }
- let(:table_name) { 'achievements' }
-
- include_examples 'schema objects assertions for', 'index_stmt'
-end
diff --git a/spec/lib/gitlab/database/schema_validation/schema_objects/table_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/table_spec.rb
deleted file mode 100644
index 60ea9581517..00000000000
--- a/spec/lib/gitlab/database/schema_validation/schema_objects/table_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Table, feature_category: :database do
- subject(:table) { described_class.new(name, columns) }
-
- let(:name) { 'my_table' }
- let(:column_class) { 'Gitlab::Database::SchemaValidation::SchemaObjects::Column' }
- let(:columns) do
- [
- instance_double(column_class, name: 'id', statement: 'id bigint NOT NULL', partition_key?: false),
- instance_double(column_class, name: 'col', statement: 'col text', partition_key?: false),
- instance_double(column_class, name: 'partition', statement: 'partition integer DEFAULT 1', partition_key?: true)
- ]
- end
-
- describe '#name' do
- it { expect(table.name).to eq('my_table') }
- end
-
- describe '#table_name' do
- it { expect(table.table_name).to eq('my_table') }
- end
-
- describe '#statement' do
- it { expect(table.statement).to eq('CREATE TABLE my_table (id bigint NOT NULL, col text)') }
-
- it 'ignores the partition column' do
- expect(table.statement).not_to include('partition integer DEFAULT 1')
- end
- end
-
- describe '#fetch_column_by_name' do
- it { expect(table.fetch_column_by_name('col')).not_to be_nil }
-
- it { expect(table.fetch_column_by_name('invalid')).to be_nil }
- end
-
- describe '#column_exists?' do
- it { expect(table.column_exists?('col')).to eq(true) }
-
- it { expect(table.column_exists?('invalid')).to eq(false) }
- end
-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
deleted file mode 100644
index 3c2481dfae0..00000000000
--- a/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# 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' }
- let(:table_name) { 'todos' }
-
- 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
deleted file mode 100644
index b0c056ff5db..00000000000
--- a/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'structure sql schema assertions for' do |object_exists_method, all_objects_method|
- subject(:structure_sql) { described_class.new(structure_file_path, schema_name) }
-
- let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
- let(:schema_name) { 'public' }
-
- describe "##{object_exists_method}" do
- it 'returns true when schema object exists' do
- expect(structure_sql.public_send(object_exists_method, valid_schema_object_name)).to be_truthy
- end
-
- it 'returns false when schema object does not exists' do
- expect(structure_sql.public_send(object_exists_method, 'invalid-object-name')).to be_falsey
- end
- end
-
- describe "##{all_objects_method}" do
- it 'returns all the schema objects' do
- schema_objects = structure_sql.public_send(all_objects_method)
-
- expect(schema_objects).to all(be_a(schema_object))
- expect(schema_objects.map(&:name)).to eq(expected_objects)
- end
- end
-end
-
-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
- let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index }
- let(:valid_schema_object_name) { 'index' }
- let(:expected_objects) do
- %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]
- end
-
- include_examples 'structure sql schema assertions for', 'index_exists?', 'indexes'
- end
-
- context 'when having triggers' do
- let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Trigger }
- let(:valid_schema_object_name) { 'trigger' }
- let(:expected_objects) { %w[trigger wrong_trigger missing_trigger_1 projects_loose_fk_trigger] }
-
- include_examples 'structure sql schema assertions for', 'trigger_exists?', 'triggers'
- end
-
- context 'when having tables' do
- let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Table }
- let(:valid_schema_object_name) { 'test_table' }
- let(:expected_objects) do
- %w[test_table ci_project_mirrors wrong_table extra_table_columns missing_table missing_table_columns
- operations_user_lists]
- end
-
- include_examples 'structure sql schema assertions for', 'table_exists?', 'tables'
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb b/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb
deleted file mode 100644
index 0b104e40c11..00000000000
--- a/spec/lib/gitlab/database/schema_validation/track_inconsistency_spec.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::TrackInconsistency, feature_category: :database do
- describe '#execute' do
- let(:validator) { Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes }
-
- let(:database_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
- let(:structure_sql_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (id)' }
-
- let(:structure_stmt) { PgQuery.parse(structure_sql_statement).tree.stmts.first.stmt.index_stmt }
- let(:database_stmt) { PgQuery.parse(database_statement).tree.stmts.first.stmt.index_stmt }
-
- let(:structure_sql_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index.new(structure_stmt) }
- let(:database_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index.new(database_stmt) }
-
- let(:inconsistency) do
- Gitlab::Database::SchemaValidation::Inconsistency.new(validator, structure_sql_object, database_object)
- end
-
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- subject(:execute) { described_class.new(inconsistency, project, user).execute }
-
- context 'when is not GitLab.com' do
- it 'does not create a schema inconsistency record' do
- allow(Gitlab).to receive(:com?).and_return(false)
-
- expect { execute }.not_to change { Gitlab::Database::SchemaValidation::SchemaInconsistency.count }
- end
- end
-
- context 'when the issue creation fails' do
- let(:issue_creation) { instance_double(Mutations::Issues::Create, resolve: { errors: 'error' }) }
-
- let(:convert_object) do
- instance_double('Gitlab::Database::ConvertFeatureCategoryToGroupLabel', execute: 'group_label')
- end
-
- before do
- allow(Gitlab::Database::ConvertFeatureCategoryToGroupLabel).to receive(:new).and_return(convert_object)
- allow(Mutations::Issues::Create).to receive(:new).and_return(issue_creation)
- end
-
- it 'does not create a schema inconsistency record' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- expect { execute }.not_to change { Gitlab::Database::SchemaValidation::SchemaInconsistency.count }
- end
- end
-
- context 'when a new inconsistency is found' do
- let(:convert_object) do
- instance_double('Gitlab::Database::ConvertFeatureCategoryToGroupLabel', execute: 'group_label')
- end
-
- before do
- allow(Gitlab::Database::ConvertFeatureCategoryToGroupLabel).to receive(:new).and_return(convert_object)
- project.add_developer(user)
- end
-
- it 'creates a new schema inconsistency record' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- expect { execute }.to change { Gitlab::Database::SchemaValidation::SchemaInconsistency.count }
- end
- end
-
- context 'when the schema inconsistency already exists' do
- let(:diff) do
- "-#{structure_sql_statement}\n" \
- "+#{database_statement}\n"
- end
-
- let!(:schema_inconsistency) do
- create(:schema_inconsistency, object_name: 'index_name', table_name: 'achievements',
- valitador_name: 'different_definition_indexes', diff: diff)
- end
-
- before do
- project.add_developer(user)
- end
-
- context 'when the issue has the last schema inconsistency' do
- it 'does not add a note' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- expect { execute }.not_to change { schema_inconsistency.issue.notes.count }
- end
- end
-
- context 'when the issue is outdated' do
- let!(:schema_inconsistency) do
- create(:schema_inconsistency, object_name: 'index_name', table_name: 'achievements',
- valitador_name: 'different_definition_indexes', diff: 'old_diff')
- end
-
- it 'adds a note' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- expect { execute }.to change { schema_inconsistency.issue.notes.count }.from(0).to(1)
- end
-
- it 'updates the diff' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- execute
-
- expect(schema_inconsistency.reload.diff).to eq(diff)
- end
- end
-
- context 'when the GitLab issue is open' do
- it 'does not create a new schema inconsistency record' do
- allow(Gitlab).to receive(:com?).and_return(true)
- schema_inconsistency.issue.update!(state_id: Issue.available_states[:opened])
-
- expect { execute }.not_to change { Gitlab::Database::SchemaValidation::SchemaInconsistency.count }
- end
- end
-
- context 'when the GitLab is not open' do
- let(:convert_object) do
- instance_double('Gitlab::Database::ConvertFeatureCategoryToGroupLabel', execute: 'group_label')
- end
-
- before do
- allow(Gitlab::Database::ConvertFeatureCategoryToGroupLabel).to receive(:new).and_return(convert_object)
- project.add_developer(user)
- end
-
- it 'creates a new schema inconsistency record' do
- allow(Gitlab).to receive(:com?).and_return(true)
- schema_inconsistency.issue.update!(state_id: Issue.available_states[:closed])
-
- expect { execute }.to change { Gitlab::Database::SchemaValidation::SchemaInconsistency.count }
- end
- end
- end
-
- context 'when the dictionary file is not present' do
- before do
- allow(Gitlab::Database::GitlabSchema).to receive(:dictionary_paths).and_return(['dictionary_not_found_path/'])
-
- project.add_developer(user)
- end
-
- it 'add the default labels' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- inconsistency = execute
-
- labels = inconsistency.issue.labels.map(&:name)
-
- expect(labels).to eq %w[database database-inconsistency-report type::maintenance severity::4]
- end
- end
-
- context 'when dictionary feature_categories are available' do
- let(:convert_object) do
- instance_double('Gitlab::Database::ConvertFeatureCategoryToGroupLabel', execute: 'group_label')
- end
-
- before do
- allow(Gitlab::Database::ConvertFeatureCategoryToGroupLabel).to receive(:new).and_return(convert_object)
-
- allow(Gitlab::Database::GitlabSchema).to receive(:dictionary_paths).and_return(['spec/fixtures/'])
-
- project.add_developer(user)
- end
-
- it 'add the default labels + group labels' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
- inconsistency = execute
-
- labels = inconsistency.issue.labels.map(&:name)
-
- expect(labels).to eq %w[database database-inconsistency-report type::maintenance severity::4 group_label]
- 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
deleted file mode 100644
index e8c08277d52..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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::ExtraTables,
- Gitlab::Database::SchemaValidation::Validators::ExtraTableColumns,
- Gitlab::Database::SchemaValidation::Validators::ExtraIndexes,
- Gitlab::Database::SchemaValidation::Validators::ExtraTriggers,
- Gitlab::Database::SchemaValidation::Validators::ExtraForeignKeys,
- Gitlab::Database::SchemaValidation::Validators::MissingTables,
- Gitlab::Database::SchemaValidation::Validators::MissingTableColumns,
- Gitlab::Database::SchemaValidation::Validators::MissingIndexes,
- Gitlab::Database::SchemaValidation::Validators::MissingTriggers,
- Gitlab::Database::SchemaValidation::Validators::MissingForeignKeys,
- Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTables,
- Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes,
- Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers,
- Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionForeignKeys
- ])
- 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_foreign_keys_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys_spec.rb
deleted file mode 100644
index ffebffc3ad2..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/different_definition_foreign_keys_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionForeignKeys,
- feature_category: :database do
- include_examples 'foreign key validators', described_class, ['public.wrong_definition_fk']
-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
deleted file mode 100644
index b9744c86b80..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/different_definition_indexes_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# 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_tables_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/different_definition_tables_spec.rb
deleted file mode 100644
index 746418b757e..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/different_definition_tables_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTables, feature_category: :database do
- include_examples 'table validators', described_class, ['wrong_table']
-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
deleted file mode 100644
index 4d065929708..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# 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_foreign_keys_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/extra_foreign_keys_spec.rb
deleted file mode 100644
index 053153aa214..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/extra_foreign_keys_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraForeignKeys, feature_category: :database do
- include_examples 'foreign key validators', described_class, ['public.extra_fk']
-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
deleted file mode 100644
index 842dbb42120..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/extra_indexes_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# 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_table_columns_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/extra_table_columns_spec.rb
deleted file mode 100644
index 9d17a2fffa9..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/extra_table_columns_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraTableColumns, feature_category: :database do
- include_examples 'table validators', described_class, ['extra_table_columns']
-end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/extra_tables_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/extra_tables_spec.rb
deleted file mode 100644
index edaf79e3c93..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/extra_tables_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraTables, feature_category: :database do
- include_examples 'table validators', described_class, ['extra_table']
-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
deleted file mode 100644
index d2e1c18a1ab..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# 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_foreign_keys_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/missing_foreign_keys_spec.rb
deleted file mode 100644
index a47804abb91..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/missing_foreign_keys_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingForeignKeys, feature_category: :database do
- include_examples 'foreign key validators', described_class, %w[public.fk_rails_536b96bff1 public.missing_fk]
-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
deleted file mode 100644
index c402c3a2fa7..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/missing_indexes_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# 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_table_columns_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/missing_table_columns_spec.rb
deleted file mode 100644
index de2956b4dd9..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/missing_table_columns_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingTableColumns, feature_category: :database do
- include_examples 'table validators', described_class, ['missing_table_columns']
-end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/missing_tables_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/missing_tables_spec.rb
deleted file mode 100644
index 7c80923e860..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/missing_tables_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingTables, feature_category: :database do
- missing_tables = %w[ci_project_mirrors missing_table operations_user_lists test_table]
-
- include_examples 'table validators', described_class, missing_tables
-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
deleted file mode 100644
index 87bc3ded808..00000000000
--- a/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# 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/similarity_score_spec.rb b/spec/lib/gitlab/database/similarity_score_spec.rb
index cfee70ed208..58ea5a58644 100644
--- a/spec/lib/gitlab/database/similarity_score_spec.rb
+++ b/spec/lib/gitlab/database/similarity_score_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Database::SimilarityScore do
end
let(:order_expression) do
- Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [{ column: Arel.sql('path') }]).to_sql
+ described_class.build_expression(search: search, rules: [{ column: Arel.sql('path') }]).to_sql
end
subject { query_result.take(3).map { |row| row['path'] } }
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Database::SimilarityScore do
let(:search) { 'text' }
let(:order_expression) do
- Gitlab::Database::SimilarityScore.build_expression(search: search, rules: []).to_sql
+ described_class.build_expression(search: search, rules: []).to_sql
end
it 'orders by a constant 0 value' do
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::Database::SimilarityScore do
describe 'score multiplier' do
let(:order_expression) do
- Gitlab::Database::SimilarityScore.build_expression(search: search, rules:
+ described_class.build_expression(search: search, rules:
[
{ column: Arel.sql('path'), multiplier: 1 },
{ column: Arel.sql('name'), multiplier: 0.8 }
@@ -94,13 +94,13 @@ RSpec.describe Gitlab::Database::SimilarityScore do
describe 'annotation' do
it 'annotates the generated SQL expression' do
- expression = Gitlab::Database::SimilarityScore.build_expression(search: 'test', rules:
+ expression = described_class.build_expression(search: 'test', rules:
[
{ column: Arel.sql('path'), multiplier: 1 },
{ column: Arel.sql('name'), multiplier: 0.8 }
])
- expect(Gitlab::Database::SimilarityScore).to be_order_by_similarity(expression)
+ expect(described_class).to be_order_by_similarity(expression)
end
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index ab3cd8fa5e6..d51319d462b 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -103,7 +103,7 @@ RSpec.describe Gitlab::Database, feature_category: :database do
before do
# CI config might not be configured
allow(ActiveRecord::Base.configurations).to receive(:configs_for)
- .with(env_name: 'test', name: 'ci', include_replicas: true)
+ .with(env_name: 'test', name: 'ci', include_hidden: true)
.and_return(ci_db_config)
end
@@ -215,7 +215,7 @@ RSpec.describe Gitlab::Database, feature_category: :database do
expect(Kernel)
.to receive(:warn)
.with(/You are using PostgreSQL/)
- .exactly(Gitlab::Database.database_base_models.length)
+ .exactly(described_class.database_base_models.length)
.times
subject
@@ -432,21 +432,21 @@ RSpec.describe Gitlab::Database, feature_category: :database do
describe '.database_base_models_with_gitlab_shared' do
before do
- Gitlab::Database.instance_variable_set(:@database_base_models_with_gitlab_shared, nil)
+ described_class.instance_variable_set(:@database_base_models_with_gitlab_shared, nil)
end
it 'memoizes the models' do
- expect { Gitlab::Database.database_base_models_with_gitlab_shared }.to change { Gitlab::Database.instance_variable_get(:@database_base_models_with_gitlab_shared) }.from(nil)
+ expect { described_class.database_base_models_with_gitlab_shared }.to change { Gitlab::Database.instance_variable_get(:@database_base_models_with_gitlab_shared) }.from(nil)
end
end
describe '.database_base_models_using_load_balancing' do
before do
- Gitlab::Database.instance_variable_set(:@database_base_models_using_load_balancing, nil)
+ described_class.instance_variable_set(:@database_base_models_using_load_balancing, nil)
end
it 'memoizes the models' do
- expect { Gitlab::Database.database_base_models_using_load_balancing }.to change { Gitlab::Database.instance_variable_get(:@database_base_models_using_load_balancing) }.from(nil)
+ expect { described_class.database_base_models_using_load_balancing }.to change { Gitlab::Database.instance_variable_get(:@database_base_models_using_load_balancing) }.from(nil)
end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 233dddbdad7..e39c15c8fd7 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -38,19 +38,19 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
end
it 'highlights and marks unchanged lines' do
- code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
+ code = %{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
expect(subject[2].rich_text).to eq(code)
end
it 'highlights and marks removed lines' do
- code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].rich_text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left addition">RuntimeError</span></span><span class="p"><span class="idiff addition">,</span></span><span class="idiff right addition"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].rich_text).to eq(code)
end
@@ -135,7 +135,7 @@ RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_managemen
it 'blobs are highlighted as plain text without loading all data' do
expect(diff_file.blob).not_to receive(:load_all_data!)
- expect(subject[2].rich_text).to eq(%Q{ <span id="LC7" class="line" lang=""> def popen(cmd, path=nil)</span>\n})
+ expect(subject[2].rich_text).to eq(%{ <span id="LC7" class="line" lang=""> def popen(cmd, path=nil)</span>\n})
expect(subject[2].rich_text).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
index e1135f4d546..4b2bb6cbb02 100644
--- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
+++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do
+RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile, feature_category: :mlops do
include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index ef2acc9ec92..98522c53a47 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -381,6 +381,125 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
it_behaves_like 'a new issue request'
end
end
+
+ context 'when receiving a service desk custom email address verification email' do
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_email_address_verification.eml') }
+
+ shared_examples 'an early exiting handler' do
+ it 'does not trigger the verification process and does not add an issue' do
+ expect(ServiceDesk::CustomEmailVerifications::UpdateService).to receive(:execute).exactly(0).times
+ expect { receiver.execute }.to not_change { Issue.count }
+ end
+ end
+
+ shared_examples 'a handler that does not verify the custom email' do |error_identifier|
+ it 'does not verify the custom email address' do
+ # project has no owner, so only notify verification triggerer
+ expect(Notify).to receive(:service_desk_verification_result_email).once
+
+ receiver.execute
+
+ expect(settings.reload.custom_email_enabled).to be false
+ expect(verification.reload).to have_attributes(
+ state: 'failed',
+ error: error_identifier
+ )
+ end
+ end
+
+ shared_examples 'a handler that verifies Service Desk custom email verification emails' do
+ it_behaves_like 'an early exiting handler'
+
+ context 'with valid service desk settings' do
+ let_it_be(:user) { create(:user) }
+
+ let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'custom-support-email@example.com') }
+ let!(:verification) { create(:service_desk_custom_email_verification, project: project, token: 'ZROT4ZZXA-Y6', triggerer: user) }
+
+ let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) }
+
+ before do
+ project.add_maintainer(user)
+
+ allow(message_delivery).to receive(:deliver_later)
+ allow(Notify).to receive(:service_desk_verification_result_email).and_return(message_delivery)
+ end
+
+ it 'successfully verifies the custom email address' do
+ # project has no owner, so only notify verification triggerer
+ expect(Notify).to receive(:service_desk_verification_result_email).once
+
+ receiver.execute
+
+ expect(settings.reload.custom_email_enabled).to be false
+ expect(verification.reload).to have_attributes(
+ state: 'finished',
+ error: nil
+ )
+ end
+
+ context 'and custom email address is not the configured subaddress of the project' do
+ before do
+ settings.update!(custom_email: 'custom-support-email@example.com')
+ end
+
+ it_behaves_like 'an early exiting handler'
+ end
+
+ context 'and verification tokens do not match' do
+ before do
+ verification.update!(token: 'XXXXXXXXXXXX')
+ end
+
+ it_behaves_like 'a handler that does not verify the custom email', 'incorrect_token'
+ end
+
+ context 'and verification email ingested too late' do
+ before do
+ verification.update!(triggered_at: ServiceDesk::CustomEmailVerification::TIMEFRAME.ago)
+ end
+
+ it_behaves_like 'a handler that does not verify the custom email', 'mail_not_received_within_timeframe'
+ end
+
+ context 'and from header differs from custom email address' do
+ before do
+ settings.update!(custom_email: 'different-from@example.com')
+ end
+
+ it_behaves_like 'a handler that does not verify the custom email', 'incorrect_from'
+ end
+ end
+
+ context 'when service_desk_custom_email feature flag is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it 'does not trigger the verification process and adds an issue instead' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ end
+ end
+ end
+
+ context 'when using incoming_email address' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it_behaves_like 'a handler that verifies Service Desk custom email verification emails'
+ end
+
+ context 'when using service_desk_email address' do
+ let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
+
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it_behaves_like 'a handler that verifies Service Desk custom email verification emails'
+ end
+ end
end
context 'when issue email creation fails' do
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index d38b7d9c85c..d3a4d77c58e 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::Email::Handler do
described_class.for(email, address).class
end
- expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler.handlers)
+ expect(matched_handlers.uniq).to match_array(described_class.handlers)
end
it 'can pick exactly one handler for each address' do
diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
index 7dd4ee7e25d..2632be98026 100644
--- a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
end
before do
- allow(Gitlab::Email::Hook::SmimeSignatureInterceptor).to receive(:certificate).and_return(certificate)
+ allow(described_class).to receive(:certificate).and_return(certificate)
Mail.register_interceptor(described_class)
mail.deliver_now
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index e58da2478bf..ee836fc2129 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -226,6 +226,25 @@ RSpec.describe Gitlab::Email::Receiver do
end
end
+ context "when the received field is malformed" do
+ let(:email_raw) do
+ attack = "for <<" * 100_000
+ [
+ "Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com",
+ "Received: from mail.example.com #{attack}; Thu, 13 Jun 2013 17:03:50 -0400",
+ "To: \"support@example.com\" <support@example.com>",
+ "",
+ "Email content"
+ ].join("\n")
+ end
+
+ it 'mail_metadata has no ReDos issue' do
+ Timeout.timeout(2) do
+ Gitlab::Email::Receiver.new(email_raw).mail_metadata
+ end
+ end
+ end
+
it 'requires all handlers to have a unique metric_event' do
events = Gitlab::Email::Handler.handlers.map do |handler|
handler.new(Mail::Message.new, 'gitlabhq/gitlabhq+auth_token').metrics_event
diff --git a/spec/lib/gitlab/encrypted_configuration_spec.rb b/spec/lib/gitlab/encrypted_configuration_spec.rb
index eadc2cf71a7..95e6d99f41c 100644
--- a/spec/lib/gitlab/encrypted_configuration_spec.rb
+++ b/spec/lib/gitlab/encrypted_configuration_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::EncryptedConfiguration do
describe '#write' do
it 'encrypts the file using the provided key' do
- encryptor = ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(credentials_key), cipher: 'aes-256-gcm')
+ encryptor = ActiveSupport::MessageEncryptor.new(described_class.generate_key(credentials_key), cipher: 'aes-256-gcm')
config = described_class.new(content_path: credentials_config_path, base_key: credentials_key)
config.write('sample-content')
@@ -122,7 +122,7 @@ RSpec.describe Gitlab::EncryptedConfiguration do
original_key_encryptor = encryptor(credential_key_original) # can read with the initial key
latest_key_encryptor = encryptor(credential_key_latest) # can read with the new key
both_key_encryptor = encryptor(credential_key_latest) # can read with either key
- both_key_encryptor.rotate(Gitlab::EncryptedConfiguration.generate_key(credential_key_original))
+ both_key_encryptor.rotate(described_class.generate_key(credential_key_original))
expect(original_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1')
expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1')
diff --git a/spec/lib/gitlab/error_tracking/logger_spec.rb b/spec/lib/gitlab/error_tracking/logger_spec.rb
index 1b722fc7896..e32a3c10cec 100644
--- a/spec/lib/gitlab/error_tracking/logger_spec.rb
+++ b/spec/lib/gitlab/error_tracking/logger_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::ErrorTracking::Logger do
expect(log_formatter).to receive(:generate_log).with(exception, payload).and_return(log_entry)
end
- expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(log_entry)
+ expect(described_class).to receive(:error).with(log_entry)
described_class.capture_exception(exception, **payload)
end
diff --git a/spec/lib/gitlab/error_tracking/processor/sanitize_error_message_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sanitize_error_message_processor_spec.rb
index 5ec73233e77..74b6df24178 100644
--- a/spec/lib/gitlab/error_tracking/processor/sanitize_error_message_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/sanitize_error_message_processor_spec.rb
@@ -9,7 +9,9 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SanitizeErrorMessageProcessor,
shared_examples 'processes the exception' do
it 'cleans the exception message' do
- expect(Gitlab::Sanitizers::ExceptionMessage).to receive(:clean).with('StandardError', 'raw error').and_return('cleaned')
+ expect(Gitlab::Sanitizers::ExceptionMessage).to receive(:clean).with(
+ 'StandardError', match('raw error')
+ ).and_return('cleaned')
expect(result_hash[:exception][:values].first).to include(
type: 'StandardError',
diff --git a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
index 73ebee49169..a854adca32b 100644
--- a/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
+++ b/spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::ErrorTracking::StackTraceHighlightDecorator do
[11, '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">HelloWorld</span></span>'],
[12, '<span id="LC1" class="line" lang="ruby"> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">message</span></span>'],
[13, '<span id="LC1" class="line" lang="ruby"> <span class="vi">@name</span> <span class="o">=</span> <span class="s1">\'World\'</span></span>'],
- [14, %Q[<span id="LC1" class="line" lang="ruby"> <span class="nb">puts</span> <span class="s2">"Hello </span><span class="si">\#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span></span>]],
+ [14, %[<span id="LC1" class="line" lang="ruby"> <span class="nb">puts</span> <span class="s2">"Hello </span><span class="si">\#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span></span>]],
[15, '<span id="LC1" class="line" lang="ruby"> <span class="k">end</span></span>'],
[16, '<span id="LC1" class="line" lang="ruby"><span class="k">end</span></span>']
]
diff --git a/spec/lib/gitlab/exception_log_formatter_spec.rb b/spec/lib/gitlab/exception_log_formatter_spec.rb
index 82166971603..179054e2d15 100644
--- a/spec/lib/gitlab/exception_log_formatter_spec.rb
+++ b/spec/lib/gitlab/exception_log_formatter_spec.rb
@@ -67,5 +67,53 @@ RSpec.describe Gitlab::ExceptionLogFormatter do
expect(payload['exception.sql']).to eq('SELECT SELECT FROM SELECT')
end
end
+
+ context 'when exception is a gRPC bad status' do
+ let(:unavailable_error) do
+ ::GRPC::Unavailable.new(
+ "unavailable",
+ gitaly_error_metadata: {
+ storage: 'default',
+ address: 'unix://gitaly.socket',
+ service: :ref_service,
+ rpc: :find_local_branches
+ }
+ )
+ end
+
+ context 'when the gRPC error is wrapped by ::Gitlab::Git::BaseError' do
+ let(:exception) { ::Gitlab::Git::CommandError.new(unavailable_error) }
+
+ it 'adds gitaly metadata to payload' do
+ described_class.format!(exception, payload)
+
+ expect(payload['exception.gitaly']).to eq('{:storage=>"default", :address=>"unix://gitaly.socket", :service=>:ref_service, :rpc=>:find_local_branches}')
+ end
+ end
+
+ context 'when the gRPC error is wrapped by another error' do
+ before do
+ allow(exception).to receive(:cause).and_return(unavailable_error)
+ end
+
+ it 'adds gitaly metadata to payload' do
+ described_class.format!(exception, payload)
+
+ expect(payload['exception.cause_class']).to eq('GRPC::Unavailable')
+ expect(payload['exception.gitaly']).to eq('{:storage=>"default", :address=>"unix://gitaly.socket", :service=>:ref_service, :rpc=>:find_local_branches}')
+ end
+ end
+
+ context 'when the gRPC error is not wrapped' do
+ let(:exception) { unavailable_error }
+
+ it 'adds gitaly metadata to payload' do
+ described_class.format!(exception, payload)
+
+ expect(payload['exception.cause_class']).to be_nil
+ expect(payload['exception.gitaly']).to eq('{:storage=>"default", :address=>"unix://gitaly.socket", :service=>:ref_service, :rpc=>:find_local_branches}')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 8bb649e78e0..6de7cab9c42 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -78,13 +78,13 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
- it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
+ it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"test"' }
- it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
+ it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
end
@@ -99,13 +99,13 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
- it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
+ it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
- it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
+ it { is_expected.to eq %{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
end
end
@@ -149,7 +149,7 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'milestone: %"9.0"' }
- it { is_expected.to eq %Q[milestone: #{old_project_ref}%"9.0"] }
+ it { is_expected.to eq %[milestone: #{old_project_ref}%"9.0"] }
end
context 'when referring to group milestone' do
diff --git a/spec/lib/gitlab/git/base_error_spec.rb b/spec/lib/gitlab/git/base_error_spec.rb
index d4db7cf2430..6efebd778b7 100644
--- a/spec/lib/gitlab/git/base_error_spec.rb
+++ b/spec/lib/gitlab/git/base_error_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Git::BaseError do
it { is_expected.to eq(result) }
end
- describe "When initialized with GRPC errors" do
+ describe "when initialized with GRPC errors without metadata" do
let(:grpc_error) { GRPC::DeadlineExceeded.new }
let(:git_error) { described_class.new grpc_error }
@@ -29,6 +29,33 @@ RSpec.describe Gitlab::Git::BaseError do
expect(git_error.service).to eq('git')
expect(git_error.status).to eq(4)
expect(git_error.code).to eq('deadline_exceeded')
+ expect(git_error.metadata).to eq({})
+ end
+ end
+
+ describe "when initialized with GRPC errors with metadata" do
+ let(:grpc_error) do
+ GRPC::DeadlineExceeded.new(
+ "deadline exceeded",
+ gitaly_error_metadata: {
+ storage: "default",
+ address: "unix://gitaly.socket",
+ service: :ref_service, rpc: :find_local_branches
+ }
+ )
+ end
+
+ let(:git_error) { described_class.new grpc_error }
+
+ it "has status, code, and metadata fields" do
+ expect(git_error.service).to eq('git')
+ expect(git_error.status).to eq(4)
+ expect(git_error.code).to eq('deadline_exceeded')
+ expect(git_error.metadata).to eq(
+ storage: "default",
+ address: "unix://gitaly.socket",
+ service: :ref_service, rpc: :find_local_branches
+ )
end
end
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 45d88f57c09..676ea2663d2 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Git::Blame do
let(:path) { 'CONTRIBUTING.md' }
let(:range) { nil }
- subject(:blame) { Gitlab::Git::Blame.new(repository, sha, path, range: range) }
+ subject(:blame) { described_class.new(repository, sha, path, range: range) }
let(:result) do
[].tap do |data|
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index d35d288050a..5bb4b84835d 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Git::Blob do
let_it_be(:repository) { project.repository.raw }
describe 'initialize' do
- let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
+ let(:blob) { described_class.new(name: 'test') }
it 'handles nil data' do
expect(described_class).not_to receive(:gitlab_blob_size)
@@ -20,14 +20,14 @@ RSpec.describe Gitlab::Git::Blob do
it 'records blob size' do
expect(described_class).to receive(:gitlab_blob_size).and_call_original
- Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abcd')
+ described_class.new(name: 'test', size: 4, data: 'abcd')
end
context 'when untruncated' do
it 'attempts to record gitlab_blob_truncated_false' do
expect(described_class).to receive(:gitlab_blob_truncated_false).and_call_original
- Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abcd')
+ described_class.new(name: 'test', size: 4, data: 'abcd')
end
end
@@ -35,32 +35,32 @@ RSpec.describe Gitlab::Git::Blob do
it 'attempts to record gitlab_blob_truncated_true' do
expect(described_class).to receive(:gitlab_blob_truncated_true).and_call_original
- Gitlab::Git::Blob.new(name: 'test', size: 40, data: 'abcd')
+ described_class.new(name: 'test', size: 40, data: 'abcd')
end
end
end
shared_examples '.find' do
context 'nil path' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], nil) }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], nil) }
it { expect(blob).to eq(nil) }
end
context 'utf-8 branch' do
- let(:blob) { Gitlab::Git::Blob.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb") }
+ let(:blob) { described_class.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb") }
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
end
context 'blank path' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], '') }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], '') }
it { expect(blob).to eq(nil) }
end
context 'file in subdir' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], "files/ruby/popen.rb") }
it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
@@ -72,7 +72,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'file in root' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], ".gitignore") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], ".gitignore") }
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
it { expect(blob.name).to eq(".gitignore") }
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'file in root with leading slash' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "/.gitignore") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], "/.gitignore") }
it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
it { expect(blob.name).to eq(".gitignore") }
@@ -97,13 +97,13 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'non-exist file' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "missing.rb") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], "missing.rb") }
it { expect(blob).to be_nil }
end
context 'six submodule' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], 'six') }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'six') }
it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
it { expect(blob.data).to eq('') }
@@ -119,7 +119,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'large file' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg') }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], 'files/images/6049019_460s.jpg') }
let(:blob_size) { 111803 }
let(:stub_limit) { 1000 }
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Git::Blob do
end
it 'marks the blob as binary' do
- expect(Gitlab::Git::Blob).to receive(:new)
+ expect(described_class).to receive(:new)
.with(hash_including(binary: true))
.and_call_original
@@ -167,8 +167,8 @@ RSpec.describe Gitlab::Git::Blob do
end
describe '.raw' do
- let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
- let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
+ let(:raw_blob) { described_class.raw(repository, SeedRepo::RubyBlob::ID) }
+ let(:bad_blob) { described_class.raw(repository, SeedRepo::BigCommit::ID) }
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
@@ -305,7 +305,7 @@ RSpec.describe Gitlab::Git::Blob do
describe '.batch_lfs_pointers' do
let(:non_lfs_blob) do
- Gitlab::Git::Blob.find(
+ described_class.find(
repository,
'master',
'README.md'
@@ -313,7 +313,7 @@ RSpec.describe Gitlab::Git::Blob do
end
let(:lfs_blob) do
- Gitlab::Git::Blob.find(
+ described_class.find(
repository,
TestEnv::BRANCH_SHA['master'],
'files/lfs/lfs_object.iso'
@@ -324,7 +324,7 @@ RSpec.describe Gitlab::Git::Blob do
blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
expect(blobs.count).to eq(1)
- expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ expect(blobs).to all( be_a(described_class) )
expect(blobs).to be_an(Array)
end
@@ -332,7 +332,7 @@ RSpec.describe Gitlab::Git::Blob do
blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy)
expect(blobs.count).to eq(1)
- expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ expect(blobs).to all( be_a(described_class) )
end
it 'handles empty list of IDs gracefully' do
@@ -361,7 +361,7 @@ RSpec.describe Gitlab::Git::Blob do
describe 'encoding', :aggregate_failures do
context 'file with russian text' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/russian.rb") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/russian.rb") }
it 'has the correct blob attributes' do
expect(blob.name).to eq("russian.rb")
@@ -375,7 +375,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'file with Japanese text' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/テスト.txt") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/テスト.txt") }
it 'has the correct blob attributes' do
expect(blob.name).to eq("テスト.txt")
@@ -387,7 +387,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'file with ISO-8859 text' do
- let(:blob) { Gitlab::Git::Blob.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/iso8859.txt") }
+ let(:blob) { described_class.find(repository, TestEnv::BRANCH_SHA['master'], "encoding/iso8859.txt") }
it 'has the correct blob attributes' do
expect(blob.name).to eq("iso8859.txt")
@@ -402,7 +402,7 @@ RSpec.describe Gitlab::Git::Blob do
describe 'mode' do
context 'file regular' do
let(:blob) do
- Gitlab::Git::Blob.find(
+ described_class.find(
repository,
TestEnv::BRANCH_SHA['master'],
'files/ruby/regex.rb'
@@ -417,7 +417,7 @@ RSpec.describe Gitlab::Git::Blob do
context 'file binary' do
let(:blob) do
- Gitlab::Git::Blob.find(
+ described_class.find(
repository,
TestEnv::BRANCH_SHA['with-executables'],
'files/executables/ls'
@@ -432,7 +432,7 @@ RSpec.describe Gitlab::Git::Blob do
context 'file symlink to regular' do
let(:blob) do
- Gitlab::Git::Blob.find(
+ described_class.find(
repository,
'88ce9520c07b7067f589b7f83a30b6250883115c',
'symlink'
@@ -449,7 +449,7 @@ RSpec.describe Gitlab::Git::Blob do
describe 'lfs_pointers' do
context 'file a valid lfs pointer' do
let(:blob) do
- Gitlab::Git::Blob.find(
+ described_class.find(
repository,
TestEnv::BRANCH_SHA['png-lfs'],
'files/images/emoji.png'
@@ -469,7 +469,7 @@ RSpec.describe Gitlab::Git::Blob do
describe '#load_all_data!' do
let(:full_data) { 'abcd' }
- let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abc') }
+ let(:blob) { described_class.new(name: 'test', size: 4, data: 'abc') }
subject { blob.load_all_data!(repository) }
@@ -483,7 +483,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'with a fully loaded blob' do
- let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) }
+ let(:blob) { described_class.new(name: 'test', size: 4, data: full_data) }
it "doesn't perform any loading" do
expect(repository.gitaly_blob_client).not_to receive(:get_blob)
@@ -497,7 +497,7 @@ RSpec.describe Gitlab::Git::Blob do
describe '#truncated?' do
context 'when blob.size is nil' do
- let(:nil_size_blob) { Gitlab::Git::Blob.new(name: 'test', data: 'abcd') }
+ let(:nil_size_blob) { described_class.new(name: 'test', data: 'abcd') }
it 'returns false' do
expect(nil_size_blob.truncated?).to be_falsey
@@ -505,7 +505,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'when blob.data is missing' do
- let(:nil_data_blob) { Gitlab::Git::Blob.new(name: 'test', size: 4) }
+ let(:nil_data_blob) { described_class.new(name: 'test', size: 4) }
it 'returns false' do
expect(nil_data_blob.truncated?).to be_falsey
@@ -513,7 +513,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'when the blob is truncated' do
- let(:truncated_blob) { Gitlab::Git::Blob.new(name: 'test', size: 40, data: 'abcd') }
+ let(:truncated_blob) { described_class.new(name: 'test', size: 40, data: 'abcd') }
it 'returns true' do
expect(truncated_blob.truncated?).to be_truthy
@@ -521,7 +521,7 @@ RSpec.describe Gitlab::Git::Blob do
end
context 'when the blob is untruncated' do
- let(:untruncated_blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abcd') }
+ let(:untruncated_blob) { described_class.new(name: 'test', size: 4, data: 'abcd') }
it 'returns false' do
expect(untruncated_blob.truncated?).to be_falsey
@@ -547,7 +547,7 @@ RSpec.describe Gitlab::Git::Blob do
context 'when the encoding cannot be detected' do
it 'successfully splits the data' do
data = "test\nblob"
- blob = Gitlab::Git::Blob.new(name: 'test', size: data.bytesize, data: data)
+ blob = described_class.new(name: 'test', size: data.bytesize, data: data)
expect(blob).to receive(:ruby_encoding) { nil }
expect(blob.lines).to eq(data.split("\n"))
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index e5f8918f7bb..dd9f77f0211 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -420,7 +420,7 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
commits = described_class.batch_by_oid(repository, oids)
expect(commits.count).to eq(2)
- expect(commits).to all( be_a(Gitlab::Git::Commit) )
+ expect(commits).to all( be_a(described_class) )
expect(commits.first.sha).to eq(SeedRepo::Commit::ID)
expect(commits.second.sha).to eq(SeedRepo::FirstCommit::ID)
end
@@ -476,7 +476,7 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
it 'returns signature and signed text' do
- signature, signed_text = subject
+ signature, signed_text, signer = subject.values_at(:signature, :signed_text, :signer)
expected_signature = <<~SIGNATURE
-----BEGIN PGP SIGNATURE-----
@@ -509,6 +509,7 @@ RSpec.describe Gitlab::Git::Commit, feature_category: :source_code_management do
expect(signed_text).to eq(expected_signed_text)
expect(signed_text).to be_a_binary_string
+ expect(signer).to eq(:SIGNER_USER)
end
end
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
index e8c683cf8aa..81b5aa94656 100644
--- a/spec/lib/gitlab/git/compare_spec.rb
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -5,8 +5,8 @@ require "spec_helper"
RSpec.describe Gitlab::Git::Compare do
let_it_be(:repository) { create(:project, :repository).repository.raw }
- let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) }
- let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) }
+ let(:compare) { described_class.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) }
+ let(:compare_straight) { described_class.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) }
describe '#commits' do
subject do
@@ -21,25 +21,25 @@ RSpec.describe Gitlab::Git::Compare do
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
context 'non-existing base ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
+ let(:compare) { described_class.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
it { is_expected.to be_empty }
end
context 'non-existing head ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
+ let(:compare) { described_class.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
it { is_expected.to be_empty }
end
context 'base ref is equal to head ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
+ let(:compare) { described_class.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
it { is_expected.to be_empty }
end
context 'providing nil as base ref or head ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) }
+ let(:compare) { described_class.new(repository, nil, nil) }
it { is_expected.to be_empty }
end
@@ -58,13 +58,13 @@ RSpec.describe Gitlab::Git::Compare do
it { is_expected.not_to include('LICENSE') }
context 'non-existing base ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
+ let(:compare) { described_class.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
it { is_expected.to be_empty }
end
context 'non-existing head ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
+ let(:compare) { described_class.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
it { is_expected.to be_empty }
end
@@ -78,7 +78,7 @@ RSpec.describe Gitlab::Git::Compare do
it { is_expected.to eq(false) }
context 'base ref is equal to head ref' do
- let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
+ let(:compare) { described_class.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
it { is_expected.to eq(true) }
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 5fa0447091c..72ddd0759ec 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
end
subject do
- Gitlab::Git::DiffCollection.new(
+ described_class.new(
iterator,
max_files: max_files,
max_lines: max_lines,
@@ -495,7 +495,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
end
describe 'empty collection' do
- subject { Gitlab::Git::DiffCollection.new([]) }
+ subject { described_class.new([]) }
it_behaves_like 'overflow stuff'
@@ -533,7 +533,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
describe '#each' do
context 'when diff are too large' do
let(:collection) do
- Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
+ described_class.new([{ diff: 'a' * 204800 }])
end
it 'yields Diff instances even when they are too large' do
@@ -612,7 +612,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
let(:iterator) { [fake_diff(1, 1)] * 4 }
before do
- allow(Gitlab::Git::DiffCollection)
+ allow(described_class)
.to receive(:default_limits)
.and_return({ max_files: 2, max_lines: max_lines })
end
@@ -641,7 +641,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
end
before do
- allow(Gitlab::Git::DiffCollection)
+ allow(described_class)
.to receive(:default_limits)
.and_return({ max_files: max_files, max_lines: 80 })
end
@@ -672,7 +672,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
before do
allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(1.megabyte)
- allow(Gitlab::Git::DiffCollection)
+ allow(described_class)
.to receive(:default_limits)
.and_return({ max_files: 4, max_lines: 3000 })
end
@@ -713,7 +713,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
context 'when offset_index is given' do
subject do
- Gitlab::Git::DiffCollection.new(
+ described_class.new(
iterator,
max_files: max_files,
max_lines: max_lines,
@@ -760,7 +760,7 @@ RSpec.describe Gitlab::Git::DiffCollection do
end
before do
- allow(Gitlab::Git::DiffCollection)
+ allow(described_class)
.to receive(:default_limits)
.and_return({ max_files: max_files, max_lines: 80 })
end
diff --git a/spec/lib/gitlab/git/finders/refs_finder_spec.rb b/spec/lib/gitlab/git/finders/refs_finder_spec.rb
new file mode 100644
index 00000000000..63d794d1e59
--- /dev/null
+++ b/spec/lib/gitlab/git/finders/refs_finder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::Git::Finders::RefsFinder, feature_category: :source_code_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:repository) { project.repository }
+ let(:finder) { described_class.new(repository, **params) }
+ let(:params) { {} }
+
+ describe "#execute" do
+ subject { finder.execute }
+
+ context "when :ref_type is :branches" do
+ let(:params) do
+ { search: "mast", ref_type: :branches }
+ end
+
+ it { is_expected.to be_an(Array) }
+
+ it "returns matching ref object" do
+ expect(subject.length).to eq(1)
+
+ ref = subject.first
+
+ expect(ref).to be_a(Gitaly::ListRefsResponse::Reference)
+ expect(ref.name).to eq("refs/heads/master")
+ expect(ref.target).to be_a(String)
+ end
+ end
+
+ context "when :ref_type is :tags" do
+ let(:params) do
+ { search: "v1.0.", ref_type: :tags }
+ end
+
+ it { is_expected.to be_an(Array) }
+
+ it "returns matching ref object" do
+ expect(subject.length).to eq(1)
+
+ ref = subject.first
+
+ expect(ref).to be_a(Gitaly::ListRefsResponse::Reference)
+ expect(ref.name).to eq("refs/tags/v1.0.0")
+ expect(ref.target).to be_a(String)
+ end
+ end
+
+ context "when :ref_type is invalid" do
+ let(:params) do
+ { search: "master", ref_type: nil }
+ end
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::UnknownRefTypeError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/keep_around_spec.rb b/spec/lib/gitlab/git/keep_around_spec.rb
index d6359d55646..65bed3f2ae6 100644
--- a/spec/lib/gitlab/git/keep_around_spec.rb
+++ b/spec/lib/gitlab/git/keep_around_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Gitlab::Git::KeepAround do
let(:repository) { create(:project, :repository).repository }
let(:service) { described_class.new(repository) }
+ let(:keep_around_ref_name) { "refs/#{::Repository::REF_KEEP_AROUND}/#{sample_commit.id}" }
it "does not fail if we attempt to reference bad commit" do
expect(service.kept_around?('abc1234')).to be_falsey
@@ -16,6 +17,7 @@ RSpec.describe Gitlab::Git::KeepAround do
service.execute([sample_commit.id])
expect(service.kept_around?(sample_commit.id)).to be_truthy
+ expect(repository.list_refs([keep_around_ref_name])).not_to be_empty
end
it "does not fail if writting the ref fails" do
@@ -45,4 +47,17 @@ RSpec.describe Gitlab::Git::KeepAround do
expect(service.kept_around?(another_sample_commit.id)).to be_truthy
end
end
+
+ context 'when disable_keep_around_refs feature flag is enabled' do
+ before do
+ stub_feature_flags(disable_keep_around_refs: true)
+ end
+
+ it 'does not create keep-around refs' do
+ service.execute([sample_commit.id])
+
+ expect(service.kept_around?(sample_commit.id)).to be_truthy
+ expect(repository.list_refs([keep_around_ref_name])).to be_empty
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index b137157f2d5..9ce8a674146 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -285,6 +285,28 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
subject { repository.size }
it { is_expected.to be > 0 }
+ it { is_expected.to be_a(Float) }
+
+ it "uses repository_info for size" do
+ expect(repository.gitaly_repository_client).to receive(:repository_info).and_call_original
+
+ subject
+ end
+
+ context "when use_repository_info_for_repository_size feature flag is disabled" do
+ before do
+ stub_feature_flags(use_repository_info_for_repository_size: false)
+ end
+
+ it { is_expected.to be > 0 }
+ it { is_expected.to be_a(Float) }
+
+ it "uses repository_size for size" do
+ expect(repository.gitaly_repository_client).to receive(:repository_size).and_call_original
+
+ subject
+ end
+ end
end
describe '#to_s' do
@@ -1151,7 +1173,31 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
commit_result.newrev
end
- subject { repository.new_blobs(newrevs).to_a }
+ subject { repository.new_blobs(newrevs) }
+
+ describe 'memoization' do
+ before do
+ allow(repository).to receive(:blobs).once.with(["--not", "--all", "--not", "revision1"], kind_of(Hash))
+ .and_return(['first result'])
+ repository.new_blobs(['revision1'])
+ end
+
+ it 'calls blobs only once' do
+ expect(repository.new_blobs(['revision1'])).to eq(['first result'])
+ end
+
+ context 'when called with a different revision' do
+ before do
+ allow(repository).to receive(:blobs).once.with(["--not", "--all", "--not", "revision2"], kind_of(Hash))
+ .and_return(['second result'])
+ repository.new_blobs(['revision2'])
+ end
+
+ it 'memoizes the different arguments' do
+ expect(repository.new_blobs(['revision2'])).to eq(['second result'])
+ end
+ end
+ end
shared_examples '#new_blobs with revisions' do
before do
@@ -1173,7 +1219,9 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
it 'memoizes results' do
expect(subject).to match_array(expected_blobs)
- expect(subject).to match_array(expected_blobs)
+
+ # call subject again
+ expect(repository.new_blobs(newrevs)).to match_array(expected_blobs)
end
end
@@ -2146,10 +2194,10 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
expect(repository.refs_by_oid(oid: Gitlab::Git::BLANK_SHA, limit: 0)).to eq([])
end
- it 'returns nil for an empty repo' do
+ it 'returns empty for an empty repo' do
project = create(:project)
- expect(project.repository.refs_by_oid(oid: TestEnv::BRANCH_SHA['master'], limit: 0)).to be_nil
+ expect(project.repository.refs_by_oid(oid: TestEnv::BRANCH_SHA['master'], limit: 0)).to eq([])
end
end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 2a68fa66b18..4a20e0b1156 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -160,6 +160,15 @@ RSpec.describe Gitlab::Git::Tree do
expect(cursor.next_cursor).to be_present
end
end
+
+ context 'and invalid reference is used' do
+ it 'returns no entries and nil cursor' do
+ allow(repository.gitaly_commit_client).to receive(:tree_entries).and_raise(Gitlab::Git::Index::IndexError)
+
+ expect(entries.count).to eq(0)
+ expect(cursor).to be_nil
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index 0d069d36e48..becf97bb24e 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::GitAccessSnippet do
let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
- subject(:access) { Gitlab::GitAccessSnippet.new(actor, snippet, protocol, authentication_abilities: authentication_abilities) }
+ subject(:access) { described_class.new(actor, snippet, protocol, authentication_abilities: authentication_abilities) }
describe 'when actor is a DeployKey' do
let(:actor) { build(:deploy_key) }
diff --git a/spec/lib/gitlab/gitaly_client/call_spec.rb b/spec/lib/gitlab/gitaly_client/call_spec.rb
index 099307fc4e1..c3c3c7bb2e8 100644
--- a/spec/lib/gitlab/gitaly_client/call_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/call_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::Call do
+RSpec.describe Gitlab::GitalyClient::Call, feature_category: :gitaly do
describe '#call', :request_store do
let(:client) { Gitlab::GitalyClient }
let(:storage) { 'default' }
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::GitalyClient::Call do
expect_call_details_to_match
end
- context 'when err' do
+ context 'when the call raises an standard error' do
before do
allow(client).to receive(:execute).and_raise(StandardError)
end
@@ -62,6 +62,25 @@ RSpec.describe Gitlab::GitalyClient::Call do
expect_call_details_to_match
end
end
+
+ context 'when the call raises a BadStatus error' do
+ before do
+ allow(client).to receive(:execute).and_raise(GRPC::Unavailable)
+ end
+
+ it 'attaches gitaly metadata' do
+ expect { subject }.to raise_error do |err|
+ expect(err.metadata).to eql(
+ gitaly_error_metadata: {
+ storage: storage,
+ address: client.address(storage),
+ service: service,
+ rpc: rpc
+ }
+ )
+ end
+ end
+ end
end
context 'when the response is an enumerator' do
@@ -103,7 +122,7 @@ RSpec.describe Gitlab::GitalyClient::Call do
expect_call_details_to_match(duration_higher_than: 0.1)
end
- context 'when err' do
+ context 'when the call raises an standard error' do
let(:response) do
Enumerator.new do |yielder|
sleep 0.2
@@ -119,6 +138,28 @@ RSpec.describe Gitlab::GitalyClient::Call do
expect_call_details_to_match(duration_higher_than: 0.2)
end
end
+
+ context 'when the call raises a BadStatus error' do
+ let(:response) do
+ Enumerator.new do |yielder|
+ yielder << 1
+ raise GRPC::Unavailable
+ end
+ end
+
+ it 'attaches gitaly metadata' do
+ expect { subject.to_a }.to raise_error do |err|
+ expect(err.metadata).to eql(
+ gitaly_error_metadata: {
+ storage: storage,
+ address: client.address(storage),
+ service: service,
+ rpc: rpc
+ }
+ )
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 70c4a2a71ff..fd66efe12c8 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -334,112 +334,6 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
include_examples 'uses requests format'
end
end
-
- context 'when feature flag "find_changed_paths_new_format" is disabled' do
- before do
- stub_feature_flags(find_changed_paths_new_format: false)
- end
-
- shared_examples 'uses commits format' do
- it do
- subject
- expect(Gitaly::FindChangedPathsRequest)
- .to have_received(:new).with(
- repository: repository_message,
- commits: commits,
- merge_commit_diff_mode: mapped_merge_commit_diff_mode
- )
- end
- end
-
- context 'when merge_commit_diff_mode is nil' do
- let(:merge_commit_diff_mode) { nil }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is :unspecified' do
- let(:merge_commit_diff_mode) { :unspecified }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is :include_merges' do
- let(:merge_commit_diff_mode) { :include_merges }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is invalid' do
- let(:merge_commit_diff_mode) { 'invalid' }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is :all_parents' do
- let(:merge_commit_diff_mode) { :all_parents }
-
- include_examples 'includes paths different in all parents'
-
- include_examples 'uses commits format'
- end
-
- context 'when feature flag "merge_commit_diff_modes" is disabled' do
- let(:mapped_merge_commit_diff_mode) { nil }
-
- before do
- stub_feature_flags(merge_commit_diff_modes: false)
- end
-
- context 'when merge_commit_diff_mode is nil' do
- let(:merge_commit_diff_mode) { nil }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is :unspecified' do
- let(:merge_commit_diff_mode) { :unspecified }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is :include_merges' do
- let(:merge_commit_diff_mode) { :include_merges }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is invalid' do
- let(:merge_commit_diff_mode) { 'invalid' }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
-
- context 'when merge_commit_diff_mode is :all_parents' do
- let(:merge_commit_diff_mode) { :all_parents }
-
- include_examples 'includes paths different in any parent'
-
- include_examples 'uses commits format'
- end
- end
- end
end
describe '#tree_entries' do
@@ -1144,4 +1038,38 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
end
end
end
+
+ describe '#get_commit_signatures' do
+ let(:project) { create(:project, :test_repo) }
+
+ it 'returns commit signatures for specified commit ids', :aggregate_failures do
+ without_signature = "e63f41fe459e62e1228fcef60d7189127aeba95a" # has no signature
+
+ signed_by_user = [
+ "a17a9f66543673edf0a3d1c6b93bdda3fe600f32", # has signature
+ "7b5160f9bb23a3d58a0accdbe89da13b96b1ece9" # SSH signature
+ ]
+
+ large_signed_text = "8cf8e80a5a0546e391823c250f2b26b9cf15ce88" # has signature and commit message > 4MB
+
+ signatures = client.get_commit_signatures(
+ [without_signature, large_signed_text, *signed_by_user]
+ )
+
+ expect(signatures.keys).to match_array([large_signed_text, *signed_by_user])
+
+ [large_signed_text, *signed_by_user].each do |commit_id|
+ expect(signatures[commit_id][:signature]).to be_present
+ expect(signatures[commit_id][:signer]).to eq(:SIGNER_USER)
+ end
+
+ signed_by_user.each do |commit_id|
+ commit = project.commit(commit_id)
+ expect(signatures[commit_id][:signed_text]).to include(commit.message)
+ expect(signatures[commit_id][:signed_text]).to include(commit.description)
+ end
+
+ expect(signatures[large_signed_text][:signed_text].size).to eq(4971878)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 869195a92b3..4a3607ed6db 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
let(:payload) do
{ source_sha: source_sha, branch: 'branch', target_ref: ref,
- message: message, first_parent_ref: first_parent_ref, allow_conflicts: true }
+ message: message, first_parent_ref: first_parent_ref }
end
it 'sends a user_merge_to_ref message' do
@@ -182,6 +182,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
expect(options).to be_kind_of(Hash)
expect(request.to_h).to eq(
payload.merge({
+ allow_conflicts: false,
repository: repository.gitaly_repository.to_h,
message: message.dup.force_encoding(Encoding::ASCII_8BIT),
user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h,
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index f457ba06074..08457e20ec3 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::RepositoryService do
+RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gitaly do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository) }
@@ -79,6 +79,21 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
end
+ describe '#repository_info' do
+ it 'sends a repository_info message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:repository_info)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_call_original
+
+ response = client.repository_info
+
+ expect(response.size).to be_an(Integer)
+ expect(response.references).to be_a(Gitaly::RepositoryInfoResponse::ReferencesInfo)
+ expect(response.objects).to be_a(Gitaly::RepositoryInfoResponse::ObjectsInfo)
+ end
+ end
+
describe '#get_object_directory_size' do
it 'sends a get_object_directory_size message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
diff --git a/spec/lib/gitlab/github_import/client_pool_spec.rb b/spec/lib/gitlab/github_import/client_pool_spec.rb
new file mode 100644
index 00000000000..aabb47c2cf1
--- /dev/null
+++ b/spec/lib/gitlab/github_import/client_pool_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::ClientPool, feature_category: :importers do
+ subject(:pool) { described_class.new(token_pool: %w[foo bar], per_page: 1, parallel: true) }
+
+ describe '#best_client' do
+ it 'returns the client with the most remaining requests' do
+ allow(Gitlab::GithubImport::Client).to receive(:new).and_return(
+ instance_double(
+ Gitlab::GithubImport::Client,
+ requests_remaining?: true, remaining_requests: 10, rate_limit_resets_in: 1
+ ),
+ instance_double(
+ Gitlab::GithubImport::Client,
+ requests_remaining?: true, remaining_requests: 20, rate_limit_resets_in: 2
+ )
+ )
+
+ expect(pool.best_client.remaining_requests).to eq(20)
+ end
+
+ context 'when all clients are rate limited' do
+ it 'returns the client with the closest rate limit reset time' do
+ allow(Gitlab::GithubImport::Client).to receive(:new).and_return(
+ instance_double(
+ Gitlab::GithubImport::Client,
+ requests_remaining?: false, remaining_requests: 10, rate_limit_resets_in: 10
+ ),
+ instance_double(
+ Gitlab::GithubImport::Client,
+ requests_remaining?: false, remaining_requests: 20, rate_limit_resets_in: 20
+ )
+ )
+
+ expect(pool.best_client.rate_limit_resets_in).to eq(10)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index 1692aac49f2..bf2ffda3bf1 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -58,54 +58,24 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi
describe '#execute' do
let(:importer) { described_class.new(issue, project, client) }
- context 'when :issues_full_test_search is disabled' do
- before do
- stub_feature_flags(issues_full_text_search: false)
- end
-
- it 'creates the issue and assignees but does not update search data' do
- expect(importer)
- .to receive(:create_issue)
- .and_return(10)
-
- expect(importer)
- .to receive(:create_assignees)
- .with(10)
+ it 'creates the issue and assignees and updates_search_data' do
+ expect(importer)
+ .to receive(:create_issue)
+ .and_return(10)
- expect(importer.issuable_finder)
- .to receive(:cache_database_id)
- .with(10)
+ expect(importer)
+ .to receive(:create_assignees)
+ .with(10)
- expect(importer).not_to receive(:update_search_data)
+ expect(importer.issuable_finder)
+ .to receive(:cache_database_id)
+ .with(10)
- importer.execute
- end
- end
-
- context 'when :issues_full_text_search feature is enabled' do
- before do
- stub_feature_flags(issues_full_text_search: true)
- end
+ expect(importer)
+ .to receive(:update_search_data)
+ .with(10)
- it 'creates the issue and assignees and updates_search_data' do
- expect(importer)
- .to receive(:create_issue)
- .and_return(10)
-
- expect(importer)
- .to receive(:create_assignees)
- .with(10)
-
- expect(importer.issuable_finder)
- .to receive(:cache_database_id)
- .with(10)
-
- expect(importer)
- .to receive(:update_search_data)
- .with(10)
-
- importer.execute
- end
+ importer.execute
end
end
diff --git a/spec/lib/gitlab/github_import/settings_spec.rb b/spec/lib/gitlab/github_import/settings_spec.rb
index 43e096863b8..d670aaea482 100644
--- a/spec/lib/gitlab/github_import/settings_spec.rb
+++ b/spec/lib/gitlab/github_import/settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Settings do
+RSpec.describe Gitlab::GithubImport::Settings, feature_category: :importers do
subject(:settings) { described_class.new(project) }
let_it_be(:project) { create(:project) }
@@ -55,19 +55,26 @@ RSpec.describe Gitlab::GithubImport::Settings do
describe '#write' do
let(:data_input) do
{
- single_endpoint_issue_events_import: true,
- single_endpoint_notes_import: 'false',
- attachments_import: nil,
- collaborators_import: false,
- foo: :bar
+ optional_stages: {
+ single_endpoint_issue_events_import: true,
+ single_endpoint_notes_import: 'false',
+ attachments_import: nil,
+ collaborators_import: false,
+ foo: :bar
+ },
+ additional_access_tokens: %w[foo bar]
}.stringify_keys
end
- it 'puts optional steps flags into projects import_data' do
+ it 'puts optional steps & access tokens into projects import_data' do
+ project.create_or_update_import_data(credentials: { user: 'token' })
+
settings.write(data_input)
expect(project.import_data.data['optional_stages'])
.to eq optional_stages.stringify_keys
+ expect(project.import_data.credentials.fetch(:additional_access_tokens))
+ .to eq(data_input['additional_access_tokens'])
end
end
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index b6e369cb35b..1739425c294 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache, feature_category: :importers do
let(:project) do
create(
:project,
@@ -223,75 +223,40 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
context 'when an Email address is not cached' do
let(:user) { { email: email } }
- it 'retrieves the Email address from the GitHub API' do
- expect(client).to receive(:user).with('kittens').and_return(user)
- expect(finder.email_for_github_username('kittens')).to eq(email)
- end
-
- it 'caches the Email address when an Email address is available' do
- expect(client).to receive(:user).with('kittens').and_return(user)
+ it 'retrieves and caches the Email address when an Email address is available' do
+ expect(client).to receive(:user).with('kittens').and_return(user).once
expect(Gitlab::Cache::Import::Caching)
.to receive(:write)
- .with(an_instance_of(String), email, timeout: Gitlab::Cache::Import::Caching::TIMEOUT)
-
- finder.email_for_github_username('kittens')
- end
+ .with(an_instance_of(String), email, timeout: Gitlab::Cache::Import::Caching::TIMEOUT).and_call_original
- it 'returns nil if the user does not exist' do
- expect(client)
- .to receive(:user)
- .with('kittens')
- .and_return(nil)
-
- expect(Gitlab::Cache::Import::Caching)
- .not_to receive(:write)
-
- expect(finder.email_for_github_username('kittens')).to be_nil
+ expect(finder.email_for_github_username('kittens')).to eq(email)
+ expect(finder.email_for_github_username('kittens')).to eq(email)
end
it 'shortens the timeout for Email address in cache when an Email address is private/nil from GitHub' do
user = { email: nil }
- expect(client).to receive(:user).with('kittens').and_return(user)
+ expect(client).to receive(:user).with('kittens').and_return(user).once
expect(Gitlab::Cache::Import::Caching)
- .to receive(:write).with(an_instance_of(String), nil, timeout: Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
+ .to receive(:write)
+ .with(an_instance_of(String), '', timeout: Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
+ .and_call_original
expect(finder.email_for_github_username('kittens')).to be_nil
+ expect(finder.email_for_github_username('kittens')).to be_nil
end
context 'when a username does not exist on GitHub' do
- context 'when github username inexistence is not cached' do
- it 'caches github username inexistence' do
- expect(client)
- .to receive(:user)
- .with('kittens')
- .and_raise(::Octokit::NotFound)
-
- expect(Gitlab::Cache::Import::Caching)
- .to receive(:write).with(
- described_class::INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY % 'kittens', true
- )
-
- expect(finder.email_for_github_username('kittens')).to be_nil
- end
- end
-
- context 'when github username inexistence is already cached' do
- it 'does not make request to the client' do
- expect(Gitlab::Cache::Import::Caching)
- .to receive(:read).with(described_class::EMAIL_FOR_USERNAME_CACHE_KEY % 'kittens')
-
- expect(Gitlab::Cache::Import::Caching)
- .to receive(:read).with(
- described_class::INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY % 'kittens'
- ).and_return('true')
-
- expect(client)
- .not_to receive(:user)
-
- expect(finder.email_for_github_username('kittens')).to be_nil
- end
+ it 'caches github username inexistence' do
+ expect(client)
+ .to receive(:user)
+ .with('kittens')
+ .and_raise(::Octokit::NotFound)
+ .once
+
+ expect(finder.email_for_github_username('kittens')).to be_nil
+ expect(finder.email_for_github_username('kittens')).to be_nil
end
end
end
diff --git a/spec/lib/gitlab/github_import_spec.rb b/spec/lib/gitlab/github_import_spec.rb
index 1ea9f003098..c4ed4b09f04 100644
--- a/spec/lib/gitlab/github_import_spec.rb
+++ b/spec/lib/gitlab/github_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport do
+RSpec.describe Gitlab::GithubImport, feature_category: :importers do
before do
stub_feature_flags(github_importer_lower_per_page_limit: false)
end
@@ -11,6 +11,8 @@ RSpec.describe Gitlab::GithubImport do
let(:project) { double(:project, import_url: 'http://t0ken@github.com/user/repo.git', id: 1, group: nil) }
it 'returns a new Client with a custom token' do
+ allow(project).to receive(:import_data)
+
expect(described_class::Client)
.to receive(:new)
.with('123', host: nil, parallel: true, per_page: 100)
@@ -24,6 +26,7 @@ RSpec.describe Gitlab::GithubImport do
expect(project)
.to receive(:import_data)
.and_return(import_data)
+ .twice
expect(described_class::Client)
.to receive(:new)
@@ -46,12 +49,31 @@ RSpec.describe Gitlab::GithubImport do
described_class.ghost_user_id
end
end
+
+ context 'when there are additional access tokens' do
+ it 'returns a new ClientPool containing all tokens' do
+ import_data = double(:import_data, credentials: { user: '123', additional_access_tokens: %w[foo bar] })
+
+ expect(project)
+ .to receive(:import_data)
+ .and_return(import_data)
+ .twice
+
+ expect(described_class::ClientPool)
+ .to receive(:new)
+ .with(token_pool: %w[foo bar], host: nil, parallel: true, per_page: 100)
+
+ described_class.new_client_for(project)
+ end
+ end
end
context 'GitHub Enterprise' do
let(:project) { double(:project, import_url: 'http://t0ken@github.another-domain.com/repo-org/repo.git', group: nil) }
it 'returns a new Client with a custom token' do
+ allow(project).to receive(:import_data)
+
expect(described_class::Client)
.to receive(:new)
.with('123', host: 'http://github.another-domain.com/api/v3', parallel: true, per_page: 100)
@@ -65,6 +87,7 @@ RSpec.describe Gitlab::GithubImport do
expect(project)
.to receive(:import_data)
.and_return(import_data)
+ .twice
expect(described_class::Client)
.to receive(:new)
diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
index 4345df1b018..4ff8137dbd4 100644
--- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb
+++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::GlRepository::RepoType do
let(:expected_identifier) { "project-#{expected_id}" }
let(:expected_suffix) { '' }
let(:expected_container) { project }
- let(:expected_repository) { ::Repository.new(project.full_path, project, shard: project.repository_storage, disk_path: project.disk_path, repo_type: Gitlab::GlRepository::PROJECT) }
+ let(:expected_repository) { ::Repository.new(project.full_path, project, shard: project.repository_storage, disk_path: project.disk_path, repo_type: described_class) }
end
it 'knows its type' do
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::GlRepository::RepoType do
let(:expected_identifier) { "wiki-#{expected_id}" }
let(:expected_suffix) { '.wiki' }
let(:expected_container) { wiki }
- let(:expected_repository) { ::Repository.new(wiki.full_path, wiki, shard: wiki.repository_storage, disk_path: wiki.disk_path, repo_type: Gitlab::GlRepository::WIKI) }
+ let(:expected_repository) { ::Repository.new(wiki.full_path, wiki, shard: wiki.repository_storage, disk_path: wiki.disk_path, repo_type: described_class) }
end
it 'knows its type' do
@@ -80,7 +80,7 @@ RSpec.describe Gitlab::GlRepository::RepoType do
let(:expected_id) { personal_snippet.id }
let(:expected_identifier) { "snippet-#{expected_id}" }
let(:expected_suffix) { '' }
- let(:expected_repository) { ::Repository.new(personal_snippet.full_path, personal_snippet, shard: personal_snippet.repository_storage, disk_path: personal_snippet.disk_path, repo_type: Gitlab::GlRepository::SNIPPET) }
+ let(:expected_repository) { ::Repository.new(personal_snippet.full_path, personal_snippet, shard: personal_snippet.repository_storage, disk_path: personal_snippet.disk_path, repo_type: described_class) }
let(:expected_container) { personal_snippet }
end
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::GlRepository::RepoType do
let(:expected_id) { project_snippet.id }
let(:expected_identifier) { "snippet-#{expected_id}" }
let(:expected_suffix) { '' }
- let(:expected_repository) { ::Repository.new(project_snippet.full_path, project_snippet, shard: project_snippet.repository_storage, disk_path: project_snippet.disk_path, repo_type: Gitlab::GlRepository::SNIPPET) }
+ let(:expected_repository) { ::Repository.new(project_snippet.full_path, project_snippet, shard: project_snippet.repository_storage, disk_path: project_snippet.disk_path, repo_type: described_class) }
let(:expected_container) { project_snippet }
end
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 819a5633a78..6cd5cda69b8 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Gpg::Commit do
+RSpec.describe Gitlab::Gpg::Commit, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository, path: 'sample-project') }
let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
@@ -12,15 +12,17 @@ RSpec.describe Gitlab::Gpg::Commit do
let(:user) { create(:user, email: user_email) }
let(:commit) { create(:commit, project: project, sha: commit_sha, committer_email: committer_email) }
let(:crypto) { instance_double(GPGME::Crypto) }
+ let(:signer) { :SIGNER_USER }
let(:mock_signature_data?) { true }
# gpg_keys must be pre-loaded so that they can be found during signature verification.
let!(:gpg_key) { create(:gpg_key, key: public_key, user: user) }
let(:signature_data) do
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
+ {
+ signature: GpgHelpers::User1.signed_commit_signature,
+ signed_text: GpgHelpers::User1.signed_commit_base_data,
+ signer: signer
+ }
end
before do
@@ -55,11 +57,12 @@ RSpec.describe Gitlab::Gpg::Commit do
context 'invalid signature' do
let(:signature_data) do
- [
+ {
# Corrupt the key
- GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
- GpgHelpers::User1.signed_commit_base_data
- ]
+ signature: GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
+ signed_text: GpgHelpers::User1.signed_commit_base_data,
+ signer: signer
+ }
end
it 'returns nil' do
@@ -185,10 +188,11 @@ RSpec.describe Gitlab::Gpg::Commit do
end
let(:signature_data) do
- [
- GpgHelpers::User3.signed_commit_signature,
- GpgHelpers::User3.signed_commit_base_data
- ]
+ {
+ signature: GpgHelpers::User3.signed_commit_signature,
+ signed_text: GpgHelpers::User3.signed_commit_base_data,
+ signer: signer
+ }
end
it 'returns a valid signature' do
@@ -339,6 +343,25 @@ RSpec.describe Gitlab::Gpg::Commit do
expect(recorder.count).to eq(1)
end
end
+
+ context 'when signature created by GitLab' do
+ let(:signer) { :SIGNER_SYSTEM }
+ let(:gpg_key) { nil }
+
+ it 'returns a valid signature' do
+ expect(described_class.new(commit).signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: nil,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: nil,
+ gpg_key_user_email: nil,
+ verification_status: 'verified_system'
+ )
+ end
+
+ it_behaves_like 'returns the cached signature on second call'
+ end
end
describe '#update_signature!' do
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index 5d444775e53..db88e99970c 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -4,7 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
describe '#run' do
- let(:signature) { [GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data] }
+ let(:signature) do
+ {
+ signature: GpgHelpers::User1.signed_commit_signature,
+ signed_text: GpgHelpers::User1.signed_commit_base_data
+ }
+ end
+
let(:committer_email) { GpgHelpers::User1.emails.first }
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
let!(:project) { create :project, :repository, path: 'sample-project' }
@@ -183,7 +189,13 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
end
context 'gpg signature did not have an associated gpg subkey' do
- let(:signature) { [GpgHelpers::User3.signed_commit_signature, GpgHelpers::User3.signed_commit_base_data] }
+ let(:signature) do
+ {
+ signature: GpgHelpers::User3.signed_commit_signature,
+ signed_text: GpgHelpers::User3.signed_commit_base_data
+ }
+ end
+
let(:committer_email) { GpgHelpers::User3.emails.first }
let!(:user) { create :user, email: GpgHelpers::User3.emails.first }
diff --git a/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb
index 449096a6faf..1bf97e87708 100644
--- a/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb
+++ b/spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb
@@ -20,14 +20,6 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::ResponseLogger do
it { expect(subject).to eq({ response_bytes: response1.bytesize + response2.bytesize }) }
end
- context 'with log_response_length disabled' do
- before do
- stub_feature_flags(log_response_length: false)
- end
-
- it { expect(subject).to eq({}) }
- end
-
context 'when response is a String' do
let(:response) { response1 }
diff --git a/spec/lib/gitlab/graphql/generic_tracing_spec.rb b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
deleted file mode 100644
index 04fe7760f62..00000000000
--- a/spec/lib/gitlab/graphql/generic_tracing_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Graphql::GenericTracing, feature_category: :application_performance do
- let(:graphql_duration_seconds_histogram) { double('Gitlab::Metrics::NullMetric') }
-
- context 'when graphql_generic_tracing_metrics_deactivate is disabled' do
- before do
- stub_feature_flags(graphql_generic_tracing_metrics_deactivate: false)
- end
-
- it 'updates graphql histogram with expected labels' do
- query = 'query { users { id } }'
- tracer = described_class.new
-
- allow(tracer)
- .to receive(:graphql_duration_seconds)
- .and_return(graphql_duration_seconds_histogram)
-
- expect_metric('graphql.lex', 'lex')
- expect_metric('graphql.parse', 'parse')
- expect_metric('graphql.validate', 'validate')
- expect_metric('graphql.analyze', 'analyze_multiplex')
- expect_metric('graphql.execute', 'execute_query_lazy')
- expect_metric('graphql.execute', 'execute_multiplex')
-
- GitlabSchema.execute(query, context: { tracers: [tracer] })
- end
- end
-
- context 'when graphql_generic_tracing_metrics_deactivate is enabled' do
- it 'does not updates graphql histogram with expected labels' do
- query = 'query { users { id } }'
- tracer = described_class.new
-
- allow(tracer)
- .to receive(:graphql_duration_seconds)
- .and_return(graphql_duration_seconds_histogram)
-
- GitlabSchema.execute(query, context: { tracers: [tracer] })
-
- expect(graphql_duration_seconds_histogram)
- .not_to receive(:observe)
- end
- end
-
- context "when labkit tracing is enabled" do
- before do
- expect(Labkit::Tracing).to receive(:enabled?).and_return(true)
- end
-
- it 'yields with labkit tracing' do
- expected_tags = {
- 'component' => 'web',
- 'span.kind' => 'server',
- 'platform_key' => 'pkey',
- 'key' => 'key'
- }
-
- expect(Labkit::Tracing)
- .to receive(:with_tracing)
- .with(operation_name: "pkey.key", tags: expected_tags)
- .and_yield
-
- expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
- end
- end
-
- context "when labkit tracing is disabled" do
- before do
- expect(Labkit::Tracing).to receive(:enabled?).and_return(false)
- end
-
- it 'yields without measurement' do
- expect(Labkit::Tracing).not_to receive(:with_tracing)
-
- expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
- end
- end
-
- private
-
- def expect_metric(platform_key, key)
- expect(graphql_duration_seconds_histogram)
- .to receive(:observe)
- .with({ platform_key: platform_key, key: key }, be > 0.0)
- end
-end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index 773df9b20ee..56fef37f939 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) }
it 'returns the encoded value of the order' do
- expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect))
+ expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_fs(:inspect))
end
end
end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index d7ae6ed06a4..173131b1d5c 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::Highlight do
end
it 'highlights' do
- expected = %Q[<span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+ expected = %[<span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">)</span></span>]
expect(described_class.highlight(file_name, content)).to eq(expected)
diff --git a/spec/lib/gitlab/hook_data/emoji_builder_spec.rb b/spec/lib/gitlab/hook_data/emoji_builder_spec.rb
new file mode 100644
index 00000000000..9c9f74c7da2
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/emoji_builder_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::EmojiBuilder, feature_category: :team_planning do
+ let_it_be(:award_emoji) { create(:award_emoji) }
+
+ let(:builder) { described_class.new(award_emoji) }
+
+ describe '#build' do
+ let(:data) { builder.build }
+
+ it 'includes safe attributes' do
+ expect(data.keys).to match_array(
+ %w[
+ user_id
+ created_at
+ id
+ name
+ awardable_type
+ awardable_id
+ updated_at
+ ]
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n/pluralization_spec.rb b/spec/lib/gitlab/i18n/pluralization_spec.rb
index 857562d549c..6bfc500c8b8 100644
--- a/spec/lib/gitlab/i18n/pluralization_spec.rb
+++ b/spec/lib/gitlab/i18n/pluralization_spec.rb
@@ -2,6 +2,7 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
+require 'rails/version'
require 'gettext_i18n_rails'
RSpec.describe Gitlab::I18n::Pluralization, feature_category: :internationalization do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f6bdbc86cc5..981802ad09d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -72,6 +72,10 @@ issues:
- issuable_resource_links
work_item_type:
- issues
+- namespace
+- work_items
+- widget_definitions
+- enabled_widget_definitions
events:
- author
- project
@@ -139,6 +143,7 @@ milestone:
- boards
- milestone_releases
- releases
+- user_agent_detail
snippets:
- author
- project
@@ -249,6 +254,8 @@ merge_request_diff:
- merge_request_diff_commits
- merge_request_diff_detail
- merge_request_diff_files
+- merge_request_diff_llm_summary
+- merge_request_review_llm_summaries
merge_request_diff_commits:
- merge_request_diff
- commit_author
@@ -807,6 +814,9 @@ project:
- design_management_repository
- design_management_repository_state
- compliance_standards_adherence
+- scan_result_policy_reads
+- project_state
+- security_policy_bots
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 1d84cba3825..9a9f9e7ebdd 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -6,7 +6,7 @@ require 'spec_helper'
# Checks whether there are new attributes in models that are currently being exported as part of the
# project Import/Export feature.
# If there are new attributes, these will have to either be added to this spec in case we want them
-# to be included as part of the export, or blacklist them using the import_export.yml configuration file.
+# to be included as part of the export, or add them to excluded_attributes in 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', feature_category: :importers 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 495cefa002a..9852f6c9652 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
@@ -71,20 +71,22 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_catego
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(importable_name, 'labels')
- .and_return([[label, 0]])
end
context 'when relation object is new' do
+ before do
+ allow(relation_reader)
+ .to receive(:consume_relation)
+ .with(importable_name, 'boards')
+ .and_return([[board, 0]])
+ end
+
context 'when relation object has invalid subrelations' do
- let(:label) do
+ let(:board) do
{
- 'title' => 'test',
- 'priorities' => [LabelPriority.new, LabelPriority.new],
- 'type' => 'GroupLabel'
+ 'name' => 'test',
+ 'lists' => [List.new, List.new],
+ 'group_id' => importable.id
}
end
@@ -94,26 +96,33 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_catego
.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"
+ relation_key: 'boards',
+ error_messages: "Label can't be blank, Position can't be blank, and Position is not a number"
)
subject
- label = importable.labels.first
+ board = importable.boards.last
failure = importable.import_failures.first
expect(importable.import_failures.count).to eq(2)
- expect(label.title).to eq('test')
+ expect(board.name).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")
+ .to eq("Label can't be blank, Position can't be blank, and Position is not a number")
end
end
end
context 'when relation object is persisted' do
+ before do
+ allow(relation_reader)
+ .to receive(:consume_relation)
+ .with(importable_name, 'labels')
+ .and_return([[label, 0]])
+ end
+
context 'when relation object is invalid' do
let(:label) { create(:group_label, group: group, title: 'test') }
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 5fa8590e8fd..43794ce01a3 100644
--- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
@@ -146,6 +146,31 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
end
end
+ context 'work item types', :request_store, feature_category: :team_planning do
+ it 'returns the correct type by base type' do
+ task_type = described_class.new(WorkItems::Type, { 'base_type' => 'task' }).find
+ incident_type = described_class.new(WorkItems::Type, { 'base_type' => 'incident' }).find
+ default_type = described_class.new(WorkItems::Type, { 'base_type' => 'bad_input' }).find
+
+ expect(task_type).to eq(WorkItems::Type.default_by_type(:task))
+ expect(incident_type).to eq(WorkItems::Type.default_by_type(:incident))
+ expect(default_type).to eq(WorkItems::Type.default_by_type(:issue))
+ end
+
+ it 'caches the results' do
+ builder = described_class.new(WorkItems::Type, { 'base_type' => 'task' })
+
+ # Make sure finder works
+ expect(builder.find).to be_a(WorkItems::Type)
+
+ query_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ builder.find
+ end.count
+
+ expect(query_count).to be_zero
+ end
+ end
+
context 'merge_request' do
it 'finds the existing merge_request' do
merge_request = create(:merge_request, title: 'MergeRequest', iid: 7, target_project: project, source_project: project)
diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
index d133f54ade5..7252457849e 100644
--- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
@@ -9,10 +9,11 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
let(:admin) { create(:admin) }
let(:importer_user) { admin }
let(:excluded_keys) { [] }
+ let(:additional_relation_attributes) { {} }
let(:created_object) do
described_class.create( # rubocop:disable Rails/SaveBang
relation_sym: relation_sym,
- relation_hash: relation_hash,
+ relation_hash: relation_hash.merge(additional_relation_attributes),
relation_index: 1,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper,
@@ -63,6 +64,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
'job_events' => false,
'wiki_page_events' => true,
'releases_events' => false,
+ 'emoji_events' => false,
'token' => token
}
end
@@ -239,6 +241,34 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
end
end
end
+
+ context 'when issue_type is provided in the hash' do
+ let(:additional_relation_attributes) { { 'issue_type' => 'task' } }
+
+ it 'sets the correct work_item_type' do
+ expect(created_object.work_item_type).to eq(WorkItems::Type.default_by_type(:task))
+ end
+ end
+
+ context 'when work_item_type is provided in the hash' do
+ let(:incident_type) { WorkItems::Type.default_by_type(:incident) }
+ let(:additional_relation_attributes) { { 'work_item_type' => incident_type } }
+
+ it 'sets the correct work_item_type' do
+ expect(created_object.work_item_type).to eq(incident_type)
+ end
+ end
+
+ context 'when issue_type is provided in the hash as well as a work_item_type' do
+ let(:incident_type) { WorkItems::Type.default_by_type(:incident) }
+ let(:additional_relation_attributes) do
+ { 'issue_type' => 'task', 'work_item_type' => incident_type }
+ end
+
+ it 'makes work_item_type take precedence over issue_type' do
+ expect(created_object.work_item_type).to eq(incident_type)
+ end
+ end
end
context 'label object' do
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 5aa16f9508d..47003707172 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -164,6 +164,25 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
expect(pipeline_metadata.project_id).to eq(pipeline.project_id)
end
+ it 'preserves work_item_type for all issues (legacy with issue_type and new with work_item_type)',
+ :aggregate_failures do
+ task_issue1 = Issue.find_by(title: 'task by issue_type')
+ task_issue2 = Issue.find_by(title: 'task by both attributes')
+ incident_issue = Issue.find_by(title: 'incident by work_item_type')
+ issue_type = WorkItems::Type.default_by_type(:issue)
+ task_type = WorkItems::Type.default_by_type(:task)
+
+ expect(task_issue1.work_item_type).to eq(task_type)
+ expect(task_issue2.work_item_type).to eq(task_type)
+ expect(incident_issue.work_item_type).to eq(WorkItems::Type.default_by_type(:incident))
+
+ other_issue_types = Issue.preload(:work_item_type).where.not(
+ id: [task_issue1.id, task_issue2.id, incident_issue.id]
+ ).map(&:work_item_type)
+
+ expect(other_issue_types).to all(eq(issue_type))
+ end
+
it 'preserves updated_at on issues' do
issue = Issue.find_by(description: 'Aliquam enim illo et possimus.')
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 d8b8903c8ca..aa8290d7a05 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -158,6 +158,12 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
it { is_expected.not_to be_empty }
+ it 'has a work_item_type' do
+ issue = subject.first
+
+ expect(issue['work_item_type']).to eq('base_type' => 'task')
+ end
+
it 'has issue comments' do
notes = subject.first['notes']
@@ -481,7 +487,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
group: group,
approvals_before_merge: 1)
- issue = create(:issue, assignees: [user], project: project)
+ issue = create(:issue, :task, assignees: [user], project: project)
snippet = create(:project_snippet, project: project)
project_label = create(:label, project: project)
group_label = create(:group_label, group: group)
diff --git a/spec/lib/gitlab/import_export/references_configuration_spec.rb b/spec/lib/gitlab/import_export/references_configuration_spec.rb
index 84c5b564cb1..a9765a8747d 100644
--- a/spec/lib/gitlab/import_export/references_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/references_configuration_spec.rb
@@ -6,7 +6,7 @@ require 'spec_helper'
# Checks whether there are new reference attributes ending with _id in models that are currently being exported as part of the
# project Import/Export feature.
# If there are new references (foreign keys), these will have to either be replaced with actual relation
-# or to be blacklisted by using the import_export.yml configuration file.
+# or to be denylisted 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', feature_category: :importers do
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index abdd8741377..b6328994c5b 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -608,6 +608,7 @@ ProjectHook:
- confidential_note_events
- repository_update_events
- releases_events
+- emoji_events
ProtectedBranch:
- id
- project_id
@@ -905,7 +906,7 @@ List:
- max_issue_count
- max_issue_weight
- limit_metric
-ExternalPullRequest:
+Ci::ExternalPullRequest:
- id
- created_at
- updated_at
@@ -1084,3 +1085,5 @@ ApprovalProjectRulesProtectedBranch:
- protected_branch_id
- approval_project_rule_id
- branch_name
+WorkItems::Type:
+ - base_type
diff --git a/spec/lib/gitlab/import_formatter_spec.rb b/spec/lib/gitlab/import_formatter_spec.rb
index 0feff61725b..6687c4fc224 100644
--- a/spec/lib/gitlab/import_formatter_spec.rb
+++ b/spec/lib/gitlab/import_formatter_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
RSpec.describe Gitlab::ImportFormatter do
- let(:formatter) { Gitlab::ImportFormatter.new }
+ let(:formatter) { described_class.new }
describe '#comment' do
it 'creates the correct string' do
diff --git a/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb b/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb
index cb4fdeed1a1..a4b5847e480 100644
--- a/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb
+++ b/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb
@@ -7,68 +7,68 @@ RSpec.describe Gitlab::InactiveProjectsDeletionWarningTracker, :freeze_time do
describe '.notified_projects', :clean_gitlab_redis_shared_state do
before do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
end
it 'returns the list of projects for which deletion warning email has been sent' do
expected_hash = { "project:1" => Date.current.to_s }
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects).to eq(expected_hash)
+ expect(described_class.notified_projects).to eq(expected_hash)
end
end
describe '.reset_all' do
before do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
end
it 'deletes all the projects for which deletion warning email was sent' do
- Gitlab::InactiveProjectsDeletionWarningTracker.reset_all
+ described_class.reset_all
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects).to eq({})
+ expect(described_class.notified_projects).to eq({})
end
end
describe '#notified?' do
before do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
end
it 'returns true if the project has already been notified' do
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?).to eq(true)
+ expect(described_class.new(project_id).notified?).to eq(true)
end
it 'returns false if the project has not been notified' do
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(2).notified?).to eq(false)
+ expect(described_class.new(2).notified?).to eq(false)
end
end
describe '#mark_notified' do
it 'marks the project as being notified' do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?).to eq(true)
+ expect(described_class.new(project_id).notified?).to eq(true)
end
end
describe '#notification_date', :clean_gitlab_redis_shared_state do
before do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
end
it 'returns the date if a deletion warning email has been sent for a given project' do
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notification_date).to eq(Date.current.to_s)
+ expect(described_class.new(project_id).notification_date).to eq(Date.current.to_s)
end
it 'returns nil if a deletion warning email has not been sent for a given project' do
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(2).notification_date).to eq(nil)
+ expect(described_class.new(2).notification_date).to eq(nil)
end
end
describe '#scheduled_deletion_date', :clean_gitlab_redis_shared_state do
shared_examples 'returns the expected deletion date' do
it do
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).scheduled_deletion_date)
+ expect(described_class.new(project_id).scheduled_deletion_date)
.to eq(1.month.from_now.to_date.to_s)
end
end
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::InactiveProjectsDeletionWarningTracker, :freeze_time do
context 'with a stored deletion email date' do
before do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
end
it_behaves_like 'returns the expected deletion date'
@@ -93,13 +93,13 @@ RSpec.describe Gitlab::InactiveProjectsDeletionWarningTracker, :freeze_time do
describe '#reset' do
before do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).mark_notified
+ described_class.new(project_id).mark_notified
end
it 'resets the project as not being notified' do
- Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).reset
+ described_class.new(project_id).reset
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notified?).to eq(false)
+ expect(described_class.new(project_id).notified?).to eq(false)
end
end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index f3c240317c8..6271885d80e 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -4,8 +4,15 @@ require 'spec_helper'
require 'rspec-parameterized'
require 'support/helpers/rails_helpers'
-RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_shared_state, :request_store, feature_category: :scalability do
+RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :request_store, feature_category: :scalability do
using RSpec::Parameterized::TableSyntax
+ include RedisHelpers
+
+ let_it_be(:redis_store_class) { define_helper_redis_store_class }
+
+ before do
+ redis_store_class.with(&:flushdb)
+ end
describe 'read and write' do
where(:setup, :command, :expect_write, :expect_read) do
@@ -32,7 +39,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
with_them do
it 'counts bytes read and written' do
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
setup.each { |cmd| redis.call(cmd) }
RequestStore.clear!
redis.call(command)
@@ -45,18 +52,18 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
describe 'counting' do
- let(:instrumentation_class) { Gitlab::Redis::SharedState.instrumentation_class }
+ let(:instrumentation_class) { redis_store_class.instrumentation_class }
it 'counts successful requests' do
expect(instrumentation_class).to receive(:instance_count_request).with(1).and_call_original
- Gitlab::Redis::SharedState.with { |redis| redis.call(:get, 'foobar') }
+ redis_store_class.with { |redis| redis.call(:get, 'foobar') }
end
it 'counts successful pipelined requests' do
expect(instrumentation_class).to receive(:instance_count_request).with(2).and_call_original
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
redis.pipelined do |pipeline|
pipeline.call(:get, '{foobar}buz')
pipeline.call(:get, '{foobar}baz')
@@ -68,12 +75,12 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
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
+ 'ask redirection' | Redis::CommandError.new("ASK 123 127.0.0.1:6380") | :instance_count_cluster_redirection
end
with_them do
before do
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.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)
@@ -88,7 +95,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).to receive(:instance_count_request).and_call_original
expect do
- Gitlab::Redis::SharedState.with { |redis| redis.call(:auth, 'foo', 'bar') }
+ redis_store_class.with { |redis| redis.call(:auth, 'foo', 'bar') }
end.to raise_exception(Redis::CommandError)
end
end
@@ -103,7 +110,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).to receive(:increment_cross_slot_request_count).and_call_original
expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
- Gitlab::Redis::SharedState.with { |redis| redis.call(:mget, 'foo', 'bar') }
+ redis_store_class.with { |redis| redis.call(:mget, 'foo', 'bar') }
end
it 'does not count allowed cross-slot requests' do
@@ -111,7 +118,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).to receive(:increment_allowed_cross_slot_request_count).and_call_original
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- Gitlab::Redis::SharedState.with { |redis| redis.call(:mget, 'foo', 'bar') }
+ redis_store_class.with { |redis| redis.call(:mget, 'foo', 'bar') }
end
end
@@ -120,7 +127,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- Gitlab::Redis::SharedState.with { |redis| redis.call(:get, 'bar') }
+ redis_store_class.with { |redis| redis.call(:get, 'bar') }
end
end
@@ -128,7 +135,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
- Gitlab::Redis::SharedState.with { |redis| redis.call(:mget, '{foo}bar', '{foo}baz') }
+ redis_store_class.with { |redis| redis.call(:mget, '{foo}bar', '{foo}baz') }
end
end
@@ -139,14 +146,14 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
it 'still runs cross-slot validation' do
expect do
- Gitlab::Redis::SharedState.with { |redis| redis.mget('foo', 'bar') }
+ redis_store_class.with { |redis| redis.mget('foo', 'bar') }
end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
end
end
end
describe 'latency' do
- let(:instrumentation_class) { Gitlab::Redis::SharedState.instrumentation_class }
+ let(:instrumentation_class) { redis_store_class.instrumentation_class }
describe 'commands in the apdex' do
where(:command) do
@@ -161,7 +168,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).to receive(:instance_observe_duration).with(a_value > 0)
.and_call_original
- Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
+ redis_store_class.with { |redis| redis.call(*command) }
end
end
@@ -170,7 +177,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
expect(instrumentation_class).to receive(:instance_observe_duration).twice.with(a_value > 0)
.and_call_original
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
redis.pipelined do |pipeline|
pipeline.call(:get, '{foobar}buz')
pipeline.call(:get, '{foobar}baz')
@@ -180,7 +187,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
it 'raises error when keys are not from the same slot' do
expect do
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
redis.pipelined do |pipeline|
pipeline.call(:get, 'foo')
pipeline.call(:get, 'bar')
@@ -206,11 +213,11 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
with_them do
it 'skips requests we do not want in the apdex' do
- Gitlab::Redis::SharedState.with { |redis| setup.each { |cmd| redis.call(*cmd) } }
+ redis_store_class.with { |redis| setup.each { |cmd| redis.call(*cmd) } }
expect(instrumentation_class).not_to receive(:instance_observe_duration)
- Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
+ redis_store_class.with { |redis| redis.call(*command) }
end
end
@@ -218,7 +225,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
it 'skips requests that have blocking commands' do
expect(instrumentation_class).not_to receive(:instance_observe_duration)
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
redis.pipelined do |pipeline|
pipeline.call(:get, '{foobar}buz')
pipeline.call(:rpush, '{foobar}baz', 1)
diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb
index 1b7774bc229..b965a6444e8 100644
--- a/spec/lib/gitlab/instrumentation/redis_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::Instrumentation::Redis do
+ include RedisHelpers
+
def stub_storages(method, value)
described_class::STORAGES.each do |storage|
allow(storage).to receive(method) { value }
@@ -30,20 +32,22 @@ RSpec.describe Gitlab::Instrumentation::Redis do
it_behaves_like 'aggregation of redis storage data', :write_bytes
describe '.payload', :request_store do
+ let_it_be(:redis_store_class) { define_helper_redis_store_class }
+
before do
# If this is the first spec in a spec run that uses Redis, there
# will be an extra SELECT command to choose the right database. We
# don't want to make the spec less precise, so we force that to
# happen (if needed) first, then clear the counts.
- Gitlab::Redis::Sessions.with { |redis| redis.info }
+ redis_store_class.with { |redis| redis.info }
RequestStore.clear!
stub_rails_env('staging') # to avoid raising CrossSlotError
- Gitlab::Redis::Sessions.with { |redis| redis.mset('cache-test', 321, 'cache-test-2', 321) }
+ redis_store_class.with { |redis| redis.mset('cache-test', 321, 'cache-test-2', 321) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- Gitlab::Redis::Sessions.with { |redis| redis.mget('cache-test', 'cache-test-2') }
+ redis_store_class.with { |redis| redis.mget('cache-test', 'cache-test-2') }
end
- Gitlab::Redis::SharedState.with { |redis| redis.set('shared-state-test', 123) }
+ Gitlab::Redis::Queues.with { |redis| redis.set('shared-state-test', 123) }
end
it 'returns payload filtering out zeroed values' do
@@ -64,11 +68,11 @@ RSpec.describe Gitlab::Instrumentation::Redis do
redis_sessions_read_bytes: be >= 0,
redis_sessions_write_bytes: be >= 0,
- # Shared state results
- redis_shared_state_calls: 1,
- redis_shared_state_duration_s: be >= 0,
- redis_shared_state_read_bytes: be >= 0,
- redis_shared_state_write_bytes: be >= 0
+ # Queues results
+ redis_queues_calls: 1,
+ redis_queues_duration_s: be >= 0,
+ redis_queues_read_bytes: be >= 0,
+ redis_queues_write_bytes: be >= 0
}
expect(described_class.payload).to include(expected_payload)
diff --git a/spec/lib/gitlab/internal_events/event_definitions_spec.rb b/spec/lib/gitlab/internal_events/event_definitions_spec.rb
new file mode 100644
index 00000000000..f6f79d9d906
--- /dev/null
+++ b/spec/lib/gitlab/internal_events/event_definitions_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::InternalEvents::EventDefinitions, feature_category: :product_analytics do
+ after(:all) do
+ described_class.clear_events
+ end
+
+ context 'when using actual metric definitions' do
+ it 'they can load' do
+ expect { described_class.load_configurations }.not_to raise_error
+ end
+ end
+
+ context 'when using mock data' do
+ let(:definition1) { instance_double(Gitlab::Usage::MetricDefinition) }
+ let(:definition2) { instance_double(Gitlab::Usage::MetricDefinition) }
+ let(:events1) { { 'event1' => nil } }
+ let(:events2) { { 'event2' => nil } }
+
+ before do
+ allow(Gitlab::Usage::MetricDefinition).to receive(:metric_definitions_changed?).and_return(true)
+ allow(Gitlab::Usage::MetricDefinition).to receive(:all).and_return([definition1, definition2])
+ allow(definition1).to receive(:available?).and_return(true)
+ allow(definition2).to receive(:available?).and_return(true)
+ allow(definition1).to receive(:events).and_return(events1)
+ allow(definition2).to receive(:events).and_return(events2)
+ end
+
+ describe ".unique_property" do
+ context 'when event has valid unique value with a period', :aggregate_failures do
+ let(:events1) { { 'event1' => :'user.id' } }
+ let(:events2) { { 'event2' => :'project.id' } }
+
+ it 'is returned' do
+ expect(described_class.unique_property('event1')).to eq(:user)
+ expect(described_class.unique_property('event2')).to eq(:project)
+ end
+ end
+
+ context 'when event has no periods in unique property', :aggregate_failures do
+ let(:events1) { { 'event1' => :plan_id } }
+
+ it 'fails' do
+ expect { described_class.unique_property('event1') }
+ .to raise_error(described_class::InvalidMetricConfiguration, /Invalid unique value/)
+ end
+ end
+
+ context 'when event has more than one period in unique property' do
+ let(:events1) { { 'event1' => :'project.namespace.id' } }
+
+ it 'fails' do
+ expect { described_class.unique_property('event1') }
+ .to raise_error(described_class::InvalidMetricConfiguration, /Invalid unique value/)
+ end
+ end
+
+ context 'when event does not have unique property' do
+ it 'unique fails' do
+ expect { described_class.unique_property('event1') }
+ .to raise_error(described_class::InvalidMetricConfiguration, /Unique property not defined for/)
+ end
+ end
+ end
+
+ describe ".load_configurations" do
+ context 'when unique property for event is ambiguous' do
+ let(:events1) { { 'event1' => :user_id } }
+ let(:events2) { { 'event1' => :project_id } }
+
+ it 'logs error when loading' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ .with(described_class::InvalidMetricConfiguration)
+
+ described_class.load_configurations
+ end
+ end
+ end
+
+ describe ".known_events" do
+ it 'has known events', :aggregate_failures do
+ expect(described_class.known_event?('event1')).to be_truthy
+ expect(described_class.known_event?('event2')).to be_truthy
+ expect(described_class.known_event?('event3')).to be_falsy
+ end
+
+ context 'when a metric fails to load' do
+ before do
+ allow(definition1).to receive(:available?).and_raise(ArgumentError)
+ end
+
+ it 'loads the healthy metrics', :aggregate_failures do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once)
+ expect(described_class.known_event?('event1')).to be_falsy
+ expect(described_class.known_event?('event2')).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb
index f23979fc56a..86215434ba4 100644
--- a/spec/lib/gitlab/internal_events_spec.rb
+++ b/spec/lib/gitlab/internal_events_spec.rb
@@ -7,14 +7,16 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
include SnowplowHelpers
before do
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_snowplow)
+ allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property).and_return(:user)
allow(fake_snowplow).to receive(:event)
end
def expect_redis_hll_tracking(event_name)
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to have_received(:track_event)
- .with(event_name, anything)
+ .with(event_name, values: unique_value)
end
def expect_snowplow_tracking(event_name)
@@ -33,23 +35,24 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
.with('InternalEventTracking', event_name, context: contexts)
end
- let_it_be(:user) { build(:user) }
- let_it_be(:project) { build(:project) }
+ let_it_be(:user) { build(:user, id: 1) }
+ let_it_be(:project) { build(:project, id: 2) }
let_it_be(:namespace) { project.namespace }
let(:fake_snowplow) { instance_double(Gitlab::Tracking::Destinations::Snowplow) }
let(:event_name) { 'g_edit_by_web_ide' }
+ let(:unique_value) { user.id }
it 'updates both RedisHLL and Snowplow', :aggregate_failures do
- params = { user_id: user.id, project_id: project.id, namespace_id: namespace.id }
+ params = { user: user, project: project, namespace: namespace }
described_class.track_event(event_name, **params)
expect_redis_hll_tracking(event_name)
expect_snowplow_tracking(event_name) # Add test for arguments
end
- it 'rescues error' do
- params = { user_id: user.id, project_id: project.id, namespace_id: namespace.id }
+ it 'rescues error', :aggregate_failures do
+ params = { user: user, project: project, namespace: namespace }
error = StandardError.new("something went wrong")
allow(fake_snowplow).to receive(:event).and_raise(error)
@@ -62,4 +65,68 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
expect { described_class.track_event(event_name, **params) }.not_to raise_error
end
+
+ it 'logs error on unknown event', :aggregate_failures do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ .with(described_class::UnknownEventError, event_name: 'unknown_event', kwargs: {})
+
+ expect { described_class.track_event('unknown_event') }.not_to raise_error
+ end
+
+ it 'logs error on missing property' do
+ expect { described_class.track_event(event_name, merge_request_id: 1) }.not_to raise_error
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception)
+ .with(described_class::InvalidPropertyError, event_name: event_name, kwargs: { merge_request_id: 1 })
+ end
+
+ context 'when unique property is missing' do
+ before do
+ allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property)
+ .and_raise(Gitlab::InternalEvents::EventDefinitions::InvalidMetricConfiguration)
+ end
+
+ it 'fails on missing unique property' do
+ expect { described_class.track_event(event_name, merge_request_id: 1) }.not_to raise_error
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception)
+ end
+ end
+
+ context 'when unique key is defined' do
+ let(:event_name) { 'p_ci_templates_terraform_base_latest' }
+ let(:unique_value) { project.id }
+ let(:property_name) { :project }
+
+ before do
+ allow(Gitlab::InternalEvents::EventDefinitions).to receive(:unique_property)
+ .with(event_name)
+ .and_return(property_name)
+ end
+
+ it 'is used when logging to RedisHLL', :aggregate_failures do
+ described_class.track_event(event_name, user: user, project: project)
+
+ expect_redis_hll_tracking(event_name)
+ expect_snowplow_tracking(event_name)
+ end
+
+ context 'when property is missing' do
+ it 'logs error' do
+ expect { described_class.track_event(event_name, merge_request_id: 1) }.not_to raise_error
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception)
+ .with(described_class::InvalidPropertyError, event_name: event_name, kwargs: { merge_request_id: 1 })
+ end
+ end
+
+ context 'when method does not exist on property' do
+ it 'logs error on missing method' do
+ expect { described_class.track_event(event_name, project: "a_string") }.not_to raise_error
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception)
+ .with(described_class::InvalidMethodError, event_name: event_name, kwargs: { project: 'a_string' })
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/issues/rebalancing/state_spec.rb b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
index a849330ad35..5adf1328b87 100644
--- a/spec/lib/gitlab/issues/rebalancing/state_spec.rb
+++ b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
@@ -160,12 +160,11 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
before do
generate_and_cache_issues_ids(count: 3)
rebalance_caching.cache_current_index(123)
- rebalance_caching.cache_current_project_id(456)
rebalance_caching.track_new_running_rebalance
end
it 'removes cache keys' do
- expect(check_existing_keys).to eq(4)
+ expect(check_existing_keys).to eq(3)
rebalance_caching.cleanup_cache
diff --git a/spec/lib/gitlab/jwt_authenticatable_spec.rb b/spec/lib/gitlab/jwt_authenticatable_spec.rb
index 9a06f9b91df..98c87ef627a 100644
--- a/spec/lib/gitlab/jwt_authenticatable_spec.rb
+++ b/spec/lib/gitlab/jwt_authenticatable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JwtAuthenticatable do
+RSpec.describe Gitlab::JwtAuthenticatable, feature_category: :system_access do
let(:test_class) do
Class.new do
include Gitlab::JwtAuthenticatable
@@ -198,5 +198,29 @@ RSpec.describe Gitlab::JwtAuthenticatable do
end
end
end
+
+ context 'algorithm' do
+ context 'with default algorithm' do
+ it 'accepts a correct header' do
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message) }.not_to raise_error
+ end
+ end
+
+ context 'with provided algorithm' do
+ it 'accepts a correct header' do
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message, algorithm: 'HS256') }.not_to raise_error
+ end
+
+ it 'raises an error when the header is signed with the wrong algorithm' do
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message, algorithm: 'RS256') }.to raise_error(JWT::DecodeError)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/kas/client_spec.rb b/spec/lib/gitlab/kas/client_spec.rb
index 5668c265611..e8884ce352f 100644
--- a/spec/lib/gitlab/kas/client_spec.rb
+++ b/spec/lib/gitlab/kas/client_spec.rb
@@ -77,8 +77,9 @@ RSpec.describe Gitlab::Kas::Client do
let(:request) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest) }
let(:response) { double(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesResponse, config_files: agent_configurations) }
- let(:repository) { instance_double(Gitlab::Agent::Modserver::Repository) }
- let(:gitaly_address) { instance_double(Gitlab::Agent::Modserver::GitalyAddress) }
+ let(:repository) { instance_double(Gitlab::Agent::Entity::GitalyRepository) }
+ let(:gitaly_info) { instance_double(Gitlab::Agent::Entity::GitalyInfo) }
+ let(:gitaly_features) { Feature::Gitaly.server_feature_flags }
let(:agent_configurations) { [double] }
@@ -89,16 +90,16 @@ RSpec.describe Gitlab::Kas::Client do
.with('example.kas.internal', :this_channel_is_insecure, timeout: described_class::TIMEOUT)
.and_return(stub)
- expect(Gitlab::Agent::Modserver::Repository).to receive(:new)
+ expect(Gitlab::Agent::Entity::GitalyRepository).to receive(:new)
.with(project.repository.gitaly_repository.to_h)
.and_return(repository)
- expect(Gitlab::Agent::Modserver::GitalyAddress).to receive(:new)
- .with(Gitlab::GitalyClient.connection_data(project.repository_storage))
- .and_return(gitaly_address)
+ expect(Gitlab::Agent::Entity::GitalyInfo).to receive(:new)
+ .with(Gitlab::GitalyClient.connection_data(project.repository_storage).merge(features: gitaly_features))
+ .and_return(gitaly_info)
expect(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest).to receive(:new)
- .with(repository: repository, gitaly_address: gitaly_address)
+ .with(repository: repository, gitaly_info: gitaly_info)
.and_return(request)
expect(stub).to receive(:list_agent_config_files)
@@ -112,7 +113,8 @@ RSpec.describe Gitlab::Kas::Client do
describe '#send_git_push_event' do
let(:stub) { instance_double(Gitlab::Agent::Notifications::Rpc::Notifications::Stub) }
let(:request) { instance_double(Gitlab::Agent::Notifications::Rpc::GitPushEventRequest) }
- let(:project_param) { instance_double(Gitlab::Agent::Notifications::Rpc::Project) }
+ let(:event_param) { instance_double(Gitlab::Agent::Event::GitPushEvent) }
+ let(:project_param) { instance_double(Gitlab::Agent::Event::Project) }
let(:response) { double(Gitlab::Agent::Notifications::Rpc::GitPushEventResponse) }
subject { described_class.new.send_git_push_event(project: project) }
@@ -122,12 +124,16 @@ RSpec.describe Gitlab::Kas::Client do
.with('example.kas.internal', :this_channel_is_insecure, timeout: described_class::TIMEOUT)
.and_return(stub)
- expect(Gitlab::Agent::Notifications::Rpc::Project).to receive(:new)
+ expect(Gitlab::Agent::Event::Project).to receive(:new)
.with(id: project.id, full_path: project.full_path)
.and_return(project_param)
- expect(Gitlab::Agent::Notifications::Rpc::GitPushEventRequest).to receive(:new)
+ expect(Gitlab::Agent::Event::GitPushEvent).to receive(:new)
.with(project: project_param)
+ .and_return(event_param)
+
+ expect(Gitlab::Agent::Notifications::Rpc::GitPushEventRequest).to receive(:new)
+ .with(event: event_param)
.and_return(request)
expect(stub).to receive(:git_push_event)
diff --git a/spec/lib/gitlab/kas/user_access_spec.rb b/spec/lib/gitlab/kas/user_access_spec.rb
index a8296d23a18..8a52d76215b 100644
--- a/spec/lib/gitlab/kas/user_access_spec.rb
+++ b/spec/lib/gitlab/kas/user_access_spec.rb
@@ -11,42 +11,6 @@ RSpec.describe Gitlab::Kas::UserAccess, feature_category: :deployment_management
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
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 2ead188dc93..d0b89afccdc 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
defaults = Gitlab::Kubernetes::KubeClient::DEFAULT_KUBECLIENT_OPTIONS
expect(client.kubeclient_options[:timeouts]).to eq(defaults[:timeouts])
- client = Gitlab::Kubernetes::KubeClient.new(api_url, timeouts: { read: 7 })
+ client = described_class.new(api_url, timeouts: { read: 7 })
expect(client.kubeclient_options[:timeouts][:read]).to eq(7)
expect(client.kubeclient_options[:timeouts][:open]).to eq(defaults[:timeouts][:open])
end
diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb
index 090b79c5d3c..3460a8fb080 100644
--- a/spec/lib/gitlab/lograge/custom_options_spec.rb
+++ b/spec/lib/gitlab/lograge/custom_options_spec.rb
@@ -60,16 +60,6 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
expect(subject[:response_bytes]).to eq(1234)
end
- context 'with log_response_length disabled' do
- before do
- stub_feature_flags(log_response_length: false)
- end
-
- it 'does not add the response length' do
- expect(subject).not_to include(:response_bytes)
- end
- end
-
it 'adds Cloudflare headers' do
expect(subject[:cf_ray]).to eq(event.payload[:cf_ray])
expect(subject[:cf_request_id]).to eq(event.payload[:cf_request_id])
diff --git a/spec/lib/gitlab/manifest_import/metadata_spec.rb b/spec/lib/gitlab/manifest_import/metadata_spec.rb
index c8158d3e148..c55b407088d 100644
--- a/spec/lib/gitlab/manifest_import/metadata_spec.rb
+++ b/spec/lib/gitlab/manifest_import/metadata_spec.rb
@@ -4,24 +4,29 @@ require 'spec_helper'
RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_state do
let(:user) { double(id: 1) }
- let(:repositories) do
+ let_it_be(:repositories) do
[
{ id: 'test1', url: 'http://demo.host/test1' },
{ id: 'test2', url: 'http://demo.host/test2' }
]
end
+ let_it_be(:hashtag_repositories_key) { 'manifest_import:metadata:user:{1}:repositories' }
+ let_it_be(:hashtag_group_id_key) { 'manifest_import:metadata:user:{1}:group_id' }
+ let_it_be(:repositories_key) { 'manifest_import:metadata:user:1:repositories' }
+ let_it_be(:group_id_key) { 'manifest_import:metadata:user:1:group_id' }
+
describe '#save' do
- it 'stores data in Redis with an expiry of EXPIRY_TIME' do
- status = described_class.new(user)
- repositories_key = 'manifest_import:metadata:user:1:repositories'
- group_id_key = 'manifest_import:metadata:user:1:group_id'
+ let(:status) { described_class.new(user) }
- status.save(repositories, 2)
+ subject { status.save(repositories, 2) }
+
+ it 'stores data in Redis with an expiry of EXPIRY_TIME' do
+ subject
Gitlab::Redis::SharedState.with do |redis|
- expect(redis.ttl(repositories_key)).to be_within(5).of(described_class::EXPIRY_TIME)
- expect(redis.ttl(group_id_key)).to be_within(5).of(described_class::EXPIRY_TIME)
+ expect(redis.ttl(hashtag_repositories_key)).to be_within(5).of(described_class::EXPIRY_TIME)
+ expect(redis.ttl(hashtag_group_id_key)).to be_within(5).of(described_class::EXPIRY_TIME)
end
end
end
@@ -41,6 +46,16 @@ RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_stat
expect(status.repositories).to eq(repositories)
end
+
+ it 'reads non-hash-tagged keys if hash-tag keys are missing' do
+ status = described_class.new(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(repositories_key, Gitlab::Json.dump(repositories))
+ end
+
+ expect(status.repositories).to eq(repositories)
+ end
end
describe '#group_id' do
@@ -58,5 +73,13 @@ RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_stat
expect(status.group_id).to eq(3)
end
+
+ it 'reads non-hash-tagged keys if hash-tag keys are missing' do
+ status = described_class.new(user)
+
+ Gitlab::Redis::SharedState.with { |redis| redis.set(group_id_key, 2) }
+
+ expect(status.group_id).to eq(2)
+ end
end
end
diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
index 2246272d3af..e33c60ef592 100644
--- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
@@ -147,7 +147,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
describe '.attributes' do
- it 'excludes cache attributes that is blacklisted by default' do
+ it 'excludes cache attributes that are denylisted by default' do
expect(thing.attributes.keys.sort).not_to include(%w[description_html])
end
end
diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
index da5431a370b..071f6e090c6 100644
--- a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
@@ -65,9 +65,7 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach
Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:pipelined).and_call_original
- times = Gitlab::Redis::ClusterUtil.cluster?(redis) ? 2 : 1
-
- expect_next_instances_of(Redis::PipelinedConnection, times) do |pipeline|
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
expect(pipeline).to receive(:mapped_hmget).once.and_call_original
end
end
diff --git a/spec/lib/gitlab/memory/reporter_spec.rb b/spec/lib/gitlab/memory/reporter_spec.rb
index 64ae740a5d7..1d19d7129cf 100644
--- a/spec/lib/gitlab/memory/reporter_spec.rb
+++ b/spec/lib/gitlab/memory/reporter_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Memory::Reporter, :aggregate_failures, feature_category:
end
context 'when cause was compression command failing' do
- let(:error_message) { "StandardError: exit 1: cat:" }
+ let(:error_message) { "StandardError.+exit 1: cat:" }
before do
stub_const('Gitlab::Memory::Reporter::COMPRESS_CMD', %w[cat --bad-flag])
diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
index d49200f87cc..a035cf02da4 100644
--- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -5,78 +5,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::Dashboard::Url do
include Gitlab::Routing.url_helpers
- describe '#metrics_regex' do
- let(:environment_id) { 1 }
- let(:url_params) do
- [
- 'foo',
- 'bar',
- environment_id,
- {
- start: '2019-08-02T05:43:09.000Z',
- dashboard: 'config/prometheus/common_metrics.yml',
- group: 'awesome group',
- anchor: 'title'
- }
- ]
- end
-
- let(:expected_params) do
- {
- 'url' => url,
- 'namespace' => 'foo',
- 'project' => 'bar',
- 'environment' => '1',
- 'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
- 'anchor' => '#title'
- }
- end
-
- subject { described_class.metrics_regex }
-
- context 'for /-/environments/:environment_id/metrics route' do
- let(:url) { metrics_namespace_project_environment_url(*url_params) }
-
- it_behaves_like 'regex which matches url when expected'
- end
-
- context 'for /-/metrics?environment=:environment_id route' do
- let(:url) { namespace_project_metrics_dashboard_url(*url_params) }
- let(:url_params) do
- [
- 'namespace1',
- 'project1',
- {
- environment: environment_id,
- start: '2019-08-02T05:43:09.000Z',
- dashboard: 'config/prometheus/common_metrics.yml',
- group: 'awesome group',
- anchor: 'title'
- }
- ]
- end
-
- let(:expected_params) do
- {
- 'url' => url,
- 'namespace' => 'namespace1',
- 'project' => 'project1',
- 'environment' => environment_id.to_s,
- 'query' => "?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&environment=#{environment_id}&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z",
- 'anchor' => '#title'
- }
- end
-
- it_behaves_like 'regex which matches url when expected'
- end
-
- context 'for metrics_dashboard route' do
- let(:url) { metrics_dashboard_namespace_project_environment_url(*url_params) }
-
- it_behaves_like 'regex which matches url when expected'
- end
- end
-
describe '#clusters_regex' do
let(:url) { Gitlab::Routing.url_helpers.namespace_project_cluster_url(*url_params) }
let(:url_params) do
@@ -130,33 +58,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Url do
end
end
- describe '#grafana_regex' do
- let(:url) do
- namespace_project_grafana_api_metrics_dashboard_url(
- 'foo',
- 'bar',
- start: '2019-08-02T05:43:09.000Z',
- dashboard: 'config/prometheus/common_metrics.yml',
- group: 'awesome group',
- anchor: 'title'
- )
- end
-
- let(:expected_params) do
- {
- 'url' => url,
- 'namespace' => 'foo',
- 'project' => 'bar',
- 'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
- 'anchor' => '#title'
- }
- end
-
- subject { described_class.grafana_regex }
-
- it_behaves_like 'regex which matches url when expected'
- end
-
describe '#alert_regex' do
let(:url) { Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_prometheus_alert_url(*url_params) }
let(:url_params) do
@@ -202,12 +103,4 @@ RSpec.describe Gitlab::Metrics::Dashboard::Url do
end
end
end
-
- describe '#build_dashboard_url' do
- it 'builds the url for the dashboard endpoint' do
- url = described_class.build_dashboard_url('foo', 'bar', 1)
-
- expect(url).to match described_class.metrics_regex
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/environment_spec.rb b/spec/lib/gitlab/metrics/environment_spec.rb
index e94162e625e..4e3b1b5273e 100644
--- a/spec/lib/gitlab/metrics/environment_spec.rb
+++ b/spec/lib/gitlab/metrics/environment_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
-
-require_relative '../../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
RSpec.describe Gitlab::Metrics::Environment, feature_category: :error_budgets do
include StubENV
diff --git a/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb b/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb
index eef9a9c79e6..9536b42cd92 100644
--- a/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb
@@ -4,23 +4,30 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::SidekiqSlis, feature_category: :error_budgets do
using RSpec::Parameterized::TableSyntax
+ let(:labels) do
+ [
+ {
+ worker: "Projects::RecordTargetPlatformsWorker",
+ feature_category: "projects",
+ urgency: "low"
+ }
+ ]
+ end
+
+ describe ".initialize_execution_slis!" do
+ it "initializes the apdex and error rate SLIs" do
+ expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:sidekiq_execution, labels)
+ expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(:sidekiq_execution, labels)
- describe ".initialize_slis!" do
- let(:possible_labels) do
- [
- {
- worker: "Projects::RecordTargetPlatformsWorker",
- feature_category: "projects",
- urgency: "low"
- }
- ]
+ described_class.initialize_execution_slis!(labels)
end
+ end
- it "initializes the apdex and error rate SLIs" do
- expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:sidekiq_execution, possible_labels)
- expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(:sidekiq_execution, possible_labels)
+ describe ".initialize_queueing_slis!" do
+ it "initializes the apdex SLIs" do
+ expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:sidekiq_queueing, labels)
- described_class.initialize_slis!(possible_labels)
+ described_class.initialize_queueing_slis!(labels)
end
end
@@ -62,4 +69,29 @@ RSpec.describe Gitlab::Metrics::SidekiqSlis, feature_category: :error_budgets do
described_class.record_execution_error(labels, error)
end
end
+
+ describe ".record_queueing_apdex" do
+ where(:urgency, :duration, :success) do
+ "high" | 5 | true
+ "high" | 11 | false
+ "low" | 50 | true
+ "low" | 70 | false
+ "throttled" | 100 | true
+ "throttled" | 1_000_000 | true
+ "not_found" | 50 | true
+ "not_found" | 70 | false
+ end
+
+ with_them do
+ it "increments the apdex SLI with success based on urgency requirement" do
+ labels = { urgency: urgency }
+ expect(Gitlab::Metrics::Sli::Apdex[:sidekiq_queueing]).to receive(:increment).with(
+ labels: labels,
+ success: success
+ )
+
+ described_class.record_queueing_apdex(labels, duration)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 83d4d3fb612..a3835f9eed0 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
it_behaves_like 'unauthorized'
end
- context 'with a blacklisted ip' do
+ context 'with a denylisted ip' do
it 'returns forbidden' do
expect(Gitlab::Auth).to receive(:find_for_git_client).and_raise(Gitlab::Auth::IpBlocked)
response = go
diff --git a/spec/lib/gitlab/multi_destination_logger_spec.rb b/spec/lib/gitlab/multi_destination_logger_spec.rb
index e0d76afd9bf..53a8541bcb7 100644
--- a/spec/lib/gitlab/multi_destination_logger_spec.rb
+++ b/spec/lib/gitlab/multi_destination_logger_spec.rb
@@ -2,9 +2,6 @@
require 'spec_helper'
-class FakeLogger
-end
-
class LoggerA < Gitlab::Logger
def self.file_name_noext
'loggerA'
diff --git a/spec/lib/gitlab/observability_spec.rb b/spec/lib/gitlab/observability_spec.rb
index 5082d193197..61f69a0171a 100644
--- a/spec/lib/gitlab/observability_spec.rb
+++ b/spec/lib/gitlab/observability_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Observability, feature_category: :error_tracking do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
describe '.observability_url' do
let(:gitlab_url) { 'https://example.com' }
@@ -31,6 +34,24 @@ RSpec.describe Gitlab::Observability, feature_category: :error_tracking do
end
end
+ describe '.oauth_url' do
+ subject { described_class.oauth_url }
+
+ it { is_expected.to eq("#{described_class.observability_url}/v1/auth/start") }
+ end
+
+ describe '.tracing_url' do
+ subject { described_class.tracing_url(project) }
+
+ it { is_expected.to eq("#{described_class.observability_url}/query/#{group.id}/#{project.id}/v1/traces") }
+ end
+
+ describe '.provisioning_url' do
+ subject { described_class.provisioning_url(project) }
+
+ it { is_expected.to eq(described_class.observability_url.to_s) }
+ end
+
describe '.build_full_url' do
let_it_be(:group) { build_stubbed(:group, id: 123) }
let(:observability_url) { described_class.observability_url }
@@ -148,6 +169,27 @@ RSpec.describe Gitlab::Observability, feature_category: :error_tracking do
end
end
+ describe '.tracing_enabled?' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ it 'returns true if feature is enabled globally' do
+ expect(described_class.tracing_enabled?(project)).to eq(true)
+ end
+
+ it 'returns true if feature is enabled for the project' do
+ stub_feature_flags(observability_tracing: false)
+ stub_feature_flags(observability_tracing: project)
+
+ expect(described_class.tracing_enabled?(project)).to eq(true)
+ end
+
+ it 'returns false if feature is disabled globally' do
+ stub_feature_flags(observability_tracing: false)
+
+ expect(described_class.tracing_enabled?(project)).to eq(false)
+ end
+ end
+
describe '.allowed_for_action?' do
let(:group) { build_stubbed(:group) }
let(:user) { build_stubbed(:user) }
diff --git a/spec/lib/gitlab/pages/url_builder_spec.rb b/spec/lib/gitlab/pages/url_builder_spec.rb
new file mode 100644
index 00000000000..8e1581704cb
--- /dev/null
+++ b/spec/lib/gitlab/pages/url_builder_spec.rb
@@ -0,0 +1,227 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pages::UrlBuilder, feature_category: :pages do
+ let(:pages_enabled) { true }
+ let(:artifacts_server) { true }
+ let(:access_control) { true }
+
+ let(:port) { nil }
+ let(:host) { 'example.com' }
+
+ let(:full_path) { 'group/project' }
+ let(:project_public) { true }
+ let(:unique_domain) { 'unique-domain' }
+ let(:unique_domain_enabled) { false }
+
+ let(:project_setting) do
+ instance_double(
+ ProjectSetting,
+ pages_unique_domain: unique_domain,
+ pages_unique_domain_enabled?: unique_domain_enabled
+ )
+ end
+
+ let(:project) do
+ instance_double(
+ Project,
+ flipper_id: 'project:1', # required for the feature flag check
+ public?: project_public,
+ project_setting: project_setting,
+ full_path: full_path
+ )
+ end
+
+ subject(:builder) { described_class.new(project) }
+
+ before do
+ stub_pages_setting(
+ enabled: pages_enabled,
+ host: host,
+ url: 'http://example.com',
+ protocol: 'http',
+ artifacts_server: artifacts_server,
+ access_control: access_control,
+ port: port
+ )
+ end
+
+ describe '#pages_url' do
+ subject(:pages_url) { builder.pages_url }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+
+ context 'when namespace is upper cased' do
+ let(:full_path) { 'Group/project' }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
+
+ context 'when project is in a nested group page' do
+ let(:full_path) { 'group/subgroup/project' }
+
+ it { is_expected.to eq('http://group.example.com/subgroup/project') }
+ end
+
+ context 'when using domain pages' do
+ let(:full_path) { 'group/group.example.com' }
+
+ it { is_expected.to eq('http://group.example.com') }
+
+ context 'in development mode' do
+ let(:port) { 3010 }
+
+ before do
+ stub_rails_env('development')
+ end
+
+ it { is_expected.to eq('http://group.example.com:3010') }
+ end
+ end
+
+ context 'when not using pages_unique_domain' do
+ subject(:pages_url) { builder.pages_url(with_unique_domain: false) }
+
+ 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/project') }
+ end
+
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ context 'when pages_unique_domain_enabled is false' do
+ let(:unique_domain_enabled) { false }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
+
+ context 'when pages_unique_domain_enabled is true' do
+ let(:unique_domain_enabled) { true }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
+ end
+ end
+
+ context 'when using pages_unique_domain' do
+ subject(:pages_url) { builder.pages_url(with_unique_domain: true) }
+
+ 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/project') }
+ end
+
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ context 'when pages_unique_domain_enabled is false' do
+ let(:unique_domain_enabled) { false }
+
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
+
+ context 'when pages_unique_domain_enabled is true' do
+ let(:unique_domain_enabled) { true }
+
+ it { is_expected.to eq('http://unique-domain.example.com') }
+ end
+ end
+ end
+ end
+
+ describe '#unique_host' do
+ subject(:unique_host) { builder.unique_host }
+
+ context 'when pages_unique_domain feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ context 'when pages_unique_domain_enabled is false' do
+ let(:unique_domain_enabled) { false }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when pages_unique_domain_enabled is true' do
+ let(:unique_domain_enabled) { true }
+
+ it { is_expected.to eq('unique-domain.example.com') }
+ end
+ end
+ end
+
+ describe '#artifact_url' do
+ let(:job) { instance_double(Ci::Build, id: 1) }
+ let(:artifact) do
+ instance_double(
+ Gitlab::Ci::Build::Artifacts::Metadata::Entry,
+ name: artifact_name,
+ path: "path/#{artifact_name}")
+ end
+
+ subject(:artifact_url) { builder.artifact_url(artifact, job) }
+
+ context 'with not allowed extension' do
+ let(:artifact_name) { 'file.gif' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with allowed extension' do
+ let(:artifact_name) { 'file.txt' }
+
+ it { is_expected.to eq("http://group.example.com/-/project/-/jobs/1/artifacts/path/file.txt") }
+
+ context 'when port is configured' do
+ let(:port) { 1234 }
+
+ it { is_expected.to eq("http://group.example.com:1234/-/project/-/jobs/1/artifacts/path/file.txt") }
+ end
+ end
+ end
+
+ describe '#artifact_url_available?' do
+ let(:job) { instance_double(Ci::Build, id: 1) }
+ let(:artifact) do
+ instance_double(
+ Gitlab::Ci::Build::Artifacts::Metadata::Entry,
+ name: artifact_name,
+ path: "path/#{artifact_name}")
+ end
+
+ subject(:artifact_url_available) { builder.artifact_url_available?(artifact, job) }
+
+ context 'with not allowed extensions' do
+ let(:artifact_name) { 'file.gif' }
+
+ it { is_expected.to be false }
+ end
+
+ context 'with allowed extensions' do
+ let(:artifact_name) { 'file.txt' }
+
+ it { is_expected.to be true }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
index b5ed583b1f1..2e87c582040 100644
--- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
context 'when next page could be available' do
let(:branches) { [branch1, branch2] }
- let(:expected_next_page_link) { %Q(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") }
+ let(:expected_next_page_link) { %(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") }
it 'uses keyset pagination and adds link headers' do
expect(request_context).to receive(:header).with('Link', expected_next_page_link)
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
index cc85c897019..1c67c9e0b95 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
@@ -33,6 +33,18 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
]
end
+ let_it_be(:ignored_column_model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = 'issues'
+
+ include IgnorableColumns
+
+ ignore_column :title, remove_with: '16.4', remove_after: '2023-08-22'
+ end
+ end
+
+ let(:scope_model) { Issue }
+ let(:created_records) { issues }
let(:iterator) do
Gitlab::Pagination::Keyset::Iterator.new(
scope: scope.limit(batch_size),
@@ -79,6 +91,55 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
end
end
+ context 'when the scope model has ignored columns' do
+ let(:scope) { ignored_column_model.order(id: :desc) }
+ let(:expected_order) { ignored_column_model.where(id: issues.map(&:id)).sort_by(&:id).reverse }
+
+ let(:in_operator_optimization_options) do
+ {
+ array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id),
+ array_mapping_scope: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:project_id].eq(id_expression)) },
+ finder_query: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:id].eq(id_expression)) }
+ }
+ end
+
+ context 'when iterating records one by one' do
+ let(:batch_size) { 1 }
+
+ it_behaves_like 'correct ordering examples'
+
+ context 'when scope selects only some columns' do
+ let(:scope) { ignored_column_model.order(id: :desc).select(:id) }
+
+ it_behaves_like 'correct ordering examples'
+ end
+ end
+
+ context 'when iterating records with LIMIT 3' do
+ let(:batch_size) { 3 }
+
+ it_behaves_like 'correct ordering examples'
+
+ context 'when scope selects only some columns' do
+ let(:scope) { ignored_column_model.order(id: :desc).select(:id) }
+
+ it_behaves_like 'correct ordering examples'
+ end
+ end
+
+ context 'when loading records at once' do
+ let(:batch_size) { issues.size + 1 }
+
+ it_behaves_like 'correct ordering examples'
+
+ context 'when scope selects only some columns' do
+ let(:scope) { ignored_column_model.order(id: :desc).select(:id) }
+
+ it_behaves_like 'correct ordering examples'
+ end
+ end
+ end
+
context 'when ordering by issues.id DESC' do
let(:scope) { Issue.order(id: :desc) }
let(:expected_order) { issues.sort_by(&:id).reverse }
@@ -332,7 +393,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
end
context 'when ordering by JOIN-ed columns' do
- let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({})).reorder(order) }
+ let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({}).select(Arel.star)).reorder(order) }
let(:cte_with_issues_and_projects) do
cte_query = Issue.select('issues.id AS id', 'project_id', 'projects.id AS projects_id', 'projects.name AS projects_name').joins(:project)
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb
index 5180403b493..3fe858f33da 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::RecordLoaderStrategy do
- let(:finder_query) { -> (created_at_value, id_value) { Project.where(Project.arel_table[:id].eq(id_value)) } }
+ let(:finder_query) { -> (created_at_value, id_value) { model.where(model.arel_table[:id].eq(id_value)) } }
let(:model) { Project }
let(:keyset_scope) do
scope, _ = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(
- Project.order(:created_at, :id)
+ model.order(:created_at, :id)
)
scope
@@ -22,6 +22,16 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R
Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumns.new(keyset_order.column_definitions, model.arel_table)
end
+ let_it_be(:ignored_column_model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = 'projects'
+
+ include IgnorableColumns
+
+ ignore_column :name, remove_with: '16.4', remove_after: '2023-08-22'
+ end
+ end
+
subject(:strategy) { described_class.new(finder_query, model, order_by_columns) }
describe '#initializer_columns' do
@@ -57,4 +67,22 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R
expect(strategy.columns).to eq([expected_loader_query.chomp])
end
end
+
+ describe '#final_projections' do
+ context 'when model does not have ignored columns' do
+ it 'does not specify the selected column names' do
+ expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*")
+ end
+ end
+
+ context 'when model has ignored columns' do
+ let(:model) { ignored_column_model }
+
+ it 'specifies the selected column names' do
+ expect(strategy.final_projections).to match_array(
+ model.default_select_columns.map { |column| "(#{described_class::RECORDS_COLUMN}).#{column.name}" }
+ )
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/pagination/keyset/iterator_spec.rb b/spec/lib/gitlab/pagination/keyset/iterator_spec.rb
index eee743c5e48..afaad48d363 100644
--- a/spec/lib/gitlab/pagination/keyset/iterator_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/iterator_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
time = Time.current
iterator.each_batch(of: 2) do |relation|
- Issue.connection.execute("UPDATE issues SET updated_at = '#{time.to_s(:inspect)}' WHERE id IN (#{relation.reselect(:id).to_sql})")
+ Issue.connection.execute("UPDATE issues SET updated_at = '#{time.to_fs(:inspect)}' WHERE id IN (#{relation.reselect(:id).to_sql})")
end
expect(Issue.pluck(:updated_at)).to all(be_within(5.seconds).of(time))
diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb
index e99846ad424..05bb0bb8b3a 100644
--- a/spec/lib/gitlab/pagination/keyset/order_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
@@ -193,7 +193,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'year',
@@ -260,7 +260,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'year',
@@ -327,7 +327,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'year',
@@ -394,7 +394,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'year',
@@ -452,7 +452,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
context 'when ordering by the named function LOWER' do
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'title',
@@ -494,7 +494,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
context 'when the passed cursor values do not match with the order definition' do
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'year',
@@ -564,7 +564,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
context 'when string attribute name is given' do
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
@@ -580,7 +580,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
context 'when symbol attribute name is given' do
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: :id,
@@ -606,7 +606,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
context 'when there are additional_projections' do
let(:order) do
- order = Gitlab::Pagination::Keyset::Order.build(
+ order = described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'created_at_field',
@@ -645,6 +645,16 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
let_it_be(:user_2) { create(:user, created_at: five_months_ago) }
let_it_be(:user_3) { create(:user, created_at: 1.month.ago) }
let_it_be(:user_4) { create(:user, created_at: 2.months.ago) }
+ let_it_be(:ignored_column_model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = 'users'
+
+ include IgnorableColumns
+ include FromUnion
+
+ ignore_column :username, remove_with: '16.4', remove_after: '2023-08-22'
+ end
+ end
let(:expected_results) { [user_3, user_4, user_2, user_1] }
let(:scope) { User.order(created_at: :desc, id: :desc) }
@@ -672,6 +682,26 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
iterator_options[:use_union_optimization] = true
end
+ context 'when the scope model has ignored columns' do
+ let(:ignored_expected_results) { expected_results.map { |r| r.becomes(ignored_column_model) } } # rubocop:disable Cop/AvoidBecomes
+
+ context 'when scope selects all columns' do
+ let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc) }
+
+ it 'returns items in the correct order' do
+ expect(items).to eq(ignored_expected_results)
+ end
+ end
+
+ context 'when scope selects only specific columns' do
+ let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc).select(:id, :created_at) }
+
+ it 'returns items in the correct order' do
+ expect(items).to eq(ignored_expected_results)
+ end
+ end
+ end
+
it 'returns items in the correct order' do
expect(items).to eq(expected_results)
end
@@ -687,7 +717,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
it 'builds UNION query' do
cursor_attributes = { created_at: five_months_ago, id: user_2.id }
- order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(keyset_aware_scope)
+ order = described_class.extract_keyset_order_object(keyset_aware_scope)
query = order.apply_cursor_conditions(scope, cursor_attributes, use_union_optimization: true).to_sql
expect(query).to include('UNION ALL')
@@ -698,7 +728,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
describe '#attribute_names' do
let(:expected_attribute_names) { %w(id name) }
let(:order) do
- Gitlab::Pagination::Keyset::Order.build(
+ described_class.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
diff --git a/spec/lib/gitlab/pagination/offset_pagination_spec.rb b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
index dc32f471756..836b3cb55d6 100644
--- a/spec/lib/gitlab/pagination/offset_pagination_spec.rb
+++ b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
@@ -44,9 +44,9 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="prev"')
end
@@ -91,8 +91,8 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="last"')
expect(val).not_to include('rel="prev"')
end
@@ -113,8 +113,8 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="last"')
expect(val).not_to include('rel="prev"')
end
@@ -242,9 +242,9 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
expect_header('X-Prev-Page', '1')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
expect(val).not_to include('rel="next"')
end
@@ -291,8 +291,8 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
expect(val).not_to include('rel="prev"')
expect(val).not_to include('rel="next"')
expect(val).not_to include('page=0')
diff --git a/spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb b/spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb
deleted file mode 100644
index 14f556ff348..00000000000
--- a/spec/lib/gitlab/patch/action_cable_redis_listener_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Patch::ActionCableRedisListener do
- let(:adapter) { instance_double('ActionCable::SubscriptionAdapter::Redis') }
- let(:connection) { instance_double('Redis') }
- let(:listener) { ActionCable::SubscriptionAdapter::Redis::Listener.new(adapter, nil) }
-
- before do
- allow(Thread).to receive(:new).and_yield
- allow(adapter).to receive(:redis_connection_for_subscriptions).and_return(connection)
- end
-
- it 'catches Redis connection errors and restarts Action Cable' do
- expect(connection).to receive(:without_reconnect).and_raise Redis::ConnectionError
- expect(ActionCable).to receive_message_chain(:server, :restart)
-
- expect { listener.add_channel('test_channel', nil) }.not_to raise_error
- end
-
- it 're-raises other exceptions' do
- expect(connection).to receive(:without_reconnect).and_raise StandardError
- expect(ActionCable).not_to receive(:server)
-
- expect { listener.add_channel('test_channel', nil) }.to raise_error(StandardError)
- end
-end
diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb
index d9cac3e1064..d0947eef2d9 100644
--- a/spec/lib/gitlab/prometheus/query_variables_spec.rb
+++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Prometheus::QueryVariables do
it do
is_expected.to include(environment_filter:
- %Q[container_name!="POD",environment="#{slug}"])
+ %[container_name!="POD",environment="#{slug}"])
end
context 'without deployment platform' do
diff --git a/spec/lib/gitlab/rack_attack/request_spec.rb b/spec/lib/gitlab/rack_attack/request_spec.rb
index ae0abfd0bc5..e8433d99d15 100644
--- a/spec/lib/gitlab/rack_attack/request_spec.rb
+++ b/spec/lib/gitlab/rack_attack/request_spec.rb
@@ -258,6 +258,11 @@ RSpec.describe Gitlab::RackAttack::Request do
valid_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
other_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
+ before do
+ allow(session).to receive(:enabled?).and_return(true)
+ allow(session).to receive(:loaded?).and_return(true)
+ end
+
where(:session, :env, :expected) do
{} | {} | false
{} | { 'HTTP_X_CSRF_TOKEN' => valid_token } | false
diff --git a/spec/lib/gitlab/redis/cross_slot_spec.rb b/spec/lib/gitlab/redis/cross_slot_spec.rb
index b3eac4357e8..e2f5fcf7694 100644
--- a/spec/lib/gitlab/redis/cross_slot_spec.rb
+++ b/spec/lib/gitlab/redis/cross_slot_spec.rb
@@ -3,10 +3,18 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::CrossSlot, feature_category: :redis do
+ include RedisHelpers
+
+ let_it_be(:redis_store_class) { define_helper_redis_store_class }
+
+ before do
+ redis_store_class.with(&:flushdb)
+ end
+
describe '.pipelined' do
context 'when using redis client' do
before do
- Gitlab::Redis::Queues.with { |redis| redis.set('a', 1) }
+ redis_store_class.with { |redis| redis.set('a', 1) }
end
it 'performs redis-rb pipelined' do
@@ -14,7 +22,7 @@ RSpec.describe Gitlab::Redis::CrossSlot, feature_category: :redis do
expect(
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- Gitlab::Redis::Queues.with do |redis|
+ redis_store_class.with do |redis|
described_class::Pipeline.new(redis).pipelined do |p|
p.get('a')
p.set('b', 1)
@@ -26,16 +34,15 @@ RSpec.describe Gitlab::Redis::CrossSlot, feature_category: :redis do
end
context 'when using with MultiStore' do
- let(:multistore) do
- Gitlab::Redis::MultiStore.new(
- ::Redis.new(::Gitlab::Redis::SharedState.params),
- ::Redis.new(::Gitlab::Redis::Sessions.params),
- 'testing')
- end
+ let_it_be(:primary_db) { 1 }
+ let_it_be(:secondary_db) { 2 }
+ let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
+ let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
+ let_it_be(:multistore) { Gitlab::Redis::MultiStore.new(primary_store, secondary_store, 'testing') }
before do
- Gitlab::Redis::SharedState.with { |redis| redis.set('a', 1) }
- Gitlab::Redis::Sessions.with { |redis| redis.set('a', 1) }
+ primary_store.set('a', 1)
+ secondary_store.set('a', 1)
skip_feature_flags_yaml_validation
skip_default_enabled_yaml_check
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 80d5915b819..e15375c88c7 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -4,20 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
using RSpec::Parameterized::TableSyntax
+ include RedisHelpers
- let_it_be(:redis_store_class) do
- Class.new(Gitlab::Redis::Wrapper) do
- def config_file_name
- config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
- Rails.root.join(config_file_name).to_s
- end
-
- def self.name
- 'Sessions'
- end
- end
- end
-
+ let_it_be(:redis_store_class) { define_helper_redis_store_class }
let_it_be(:primary_db) { 1 }
let_it_be(:secondary_db) { 2 }
let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
@@ -269,8 +258,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
multi_store.default_store.flushdb
end
- it 'does not call the fallback store' do
- expect(multi_store.fallback_store).not_to receive(name)
+ it 'does not call the non_default_store' do
+ expect(multi_store.non_default_store).not_to receive(name)
end
end
@@ -574,21 +563,33 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when executing on the primary instance is raising an exception' do
+ context 'when executing on the 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 'raises error and does not execute on non default instance', :aggregate_failures do
+ expect(multi_store.non_default_store).not_to receive(name).with(*expected_args)
+ expect { subject }.to raise_error(StandardError)
+ end
+ end
+
+ context 'when executing on the non default instance is raising an exception' do
before do
- allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.non_default_store).to receive(name).with(*expected_args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
- it 'logs the exception and execute on secondary instance', :aggregate_failures do
+ it 'logs the exception and execute on default instance', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name))
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(multi_store.default_store).to receive(name).with(*expected_args).and_call_original
subject
end
- include_examples 'verify that store contains values', :secondary_store
+ include_examples 'verify that store contains values', :default_store
end
context 'when the command is executed within pipelined block' do
@@ -661,21 +662,33 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
include_examples 'verify that store contains values', :secondary_store
end
- context 'when executing on the primary instance is raising an exception' do
+ context 'when executing on the default instance is raising an exception' do
before do
- allow(primary_store).to receive(name).and_raise(StandardError)
+ allow(multi_store.default_store).to receive(name).and_raise(StandardError)
+ end
+
+ it 'raises error and does not execute on non default instance', :aggregate_failures do
+ expect(multi_store.non_default_store).not_to receive(name)
+
+ expect { subject }.to raise_error(StandardError)
+ end
+ end
+
+ context 'when executing on the non default instance is raising an exception' do
+ before do
+ allow(multi_store.non_default_store).to receive(name).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
- it 'logs the exception and execute on secondary instance', :aggregate_failures do
+ it 'logs the exception and execute on default instance', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(:multi_store_error_message, command_name: name))
- expect(secondary_store).to receive(name).and_call_original
+ expect(multi_store.default_store).to receive(name).and_call_original
subject
end
- include_examples 'verify that store contains values', :secondary_store
+ include_examples 'verify that store contains values', :default_store
end
describe 'return values from a pipelined command' do
@@ -708,15 +721,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when the value exists on both but differ' do
before do
- primary_store.set(key1, value1)
- secondary_store.set(key1, value2)
+ multi_store.non_default_store.set(key1, value1)
+ multi_store.default_store.set(key1, value2)
end
it 'returns the value from the secondary store, logging an error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
pipeline_diff_error_with_stacktrace(
'Pipelined command executed on both stores successfully but results differ between them. ' \
- "Result from the primary: [#{value1.inspect}]. Result from the secondary: [#{value2.inspect}]."
+ "Result from the non-default store: [#{value1.inspect}]. " \
+ "Result from the default store: [#{value2.inspect}]."
),
hash_including(command_name: name, instance_name: instance_name)
).and_call_original
@@ -726,16 +740,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when the value does not exist on the primary but it does on the secondary' do
+ context 'when the value does not exist on the non-default store but it does on the default' do
before do
- secondary_store.set(key1, value2)
+ multi_store.default_store.set(key1, value2)
end
it 'returns the value from the secondary store, logging an error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
pipeline_diff_error_with_stacktrace(
'Pipelined command executed on both stores successfully but results differ between them. ' \
- "Result from the primary: [nil]. Result from the secondary: [#{value2.inspect}]."
+ "Result from the non-default store: [nil]. Result from the default store: [#{value2.inspect}]."
),
hash_including(command_name: name, instance_name: instance_name)
)
@@ -784,23 +798,53 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
context 'when either store is a an instance of ::Redis::Cluster' do
+ let(:pipeline) { double }
+ let(:client) { double }
+
before do
- client = double
allow(client).to receive(:instance_of?).with(::Redis::Cluster).and_return(true)
- allow(primary_store).to receive(:_client).and_return(client)
+ allow(pipeline).to receive(:pipelined)
+ allow(multi_store.default_store).to receive(:_client).and_return(client)
end
it 'calls cross-slot pipeline within multistore' do
if name == :pipelined
# we intentionally exclude `.and_call_original` since primary_store/secondary_store
# may not be running on a proper Redis Cluster.
- expect(Gitlab::Redis::CrossSlot::Pipeline).to receive(:new).with(primary_store).exactly(:once)
- expect(Gitlab::Redis::CrossSlot::Pipeline).not_to receive(:new).with(secondary_store)
+ expect(Gitlab::Redis::CrossSlot::Pipeline).to receive(:new)
+ .with(multi_store.default_store)
+ .exactly(:once)
+ .and_return(pipeline)
+ expect(Gitlab::Redis::CrossSlot::Pipeline).not_to receive(:new).with(multi_store.non_default_store)
end
subject
end
end
+
+ context 'when with_readonly_pipeline is used' do
+ it 'calls the default store only' do
+ expect(primary_store).to receive(:send).and_call_original
+ expect(secondary_store).not_to receive(:send).and_call_original
+
+ multi_store.with_readonly_pipeline { subject }
+ end
+
+ context 'when used in a nested manner' do
+ subject(:nested_subject) do
+ multi_store.with_readonly_pipeline do
+ multi_store.with_readonly_pipeline { subject }
+ end
+ end
+
+ it 'raises error' do
+ expect { nested_subject }.to raise_error(Gitlab::Redis::MultiStore::NestedReadonlyPipelineError)
+ expect { nested_subject }.to raise_error { |e|
+ expect(e.message).to eq('Nested use of with_readonly_pipeline is detected.')
+ }
+ end
+ end
+ end
end
end
@@ -1086,8 +1130,4 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
end
-
- def create_redis_store(options, extras = {})
- ::Redis::Store.new(options.merge(extras))
- end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 62fcb4821fc..37db13b76b9 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
project.add_reporter(@u_foo)
project.add_reporter(@u_bar)
- subject.analyze(%Q{
+ subject.analyze(%{
Inline code: `@foo`
Code block:
diff --git a/spec/lib/gitlab/relative_positioning/range_spec.rb b/spec/lib/gitlab/relative_positioning/range_spec.rb
index da1f0166d5d..cb3e1504a7a 100644
--- a/spec/lib/gitlab/relative_positioning/range_spec.rb
+++ b/spec/lib/gitlab/relative_positioning/range_spec.rb
@@ -27,19 +27,19 @@ RSpec.describe Gitlab::RelativePositioning::Range do
it 'constructs a closed range when both termini are provided' do
range = Gitlab::RelativePositioning.range(item_a, item_b)
- expect(range).to be_a_kind_of(Gitlab::RelativePositioning::Range)
+ expect(range).to be_a_kind_of(described_class)
expect(range).to be_a_kind_of(Gitlab::RelativePositioning::ClosedRange)
end
it 'constructs a starting-from range when only the LHS is provided' do
range = Gitlab::RelativePositioning.range(item_a, nil)
- expect(range).to be_a_kind_of(Gitlab::RelativePositioning::Range)
+ expect(range).to be_a_kind_of(described_class)
expect(range).to be_a_kind_of(Gitlab::RelativePositioning::StartingFrom)
end
it 'constructs an ending-at range when only the RHS is provided' do
range = Gitlab::RelativePositioning.range(nil, item_b)
- expect(range).to be_a_kind_of(Gitlab::RelativePositioning::Range)
+ expect(range).to be_a_kind_of(described_class)
expect(range).to be_a_kind_of(Gitlab::RelativePositioning::EndingAt)
end
end
diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb
index 10842173365..dbf9f295706 100644
--- a/spec/lib/gitlab/request_forgery_protection_spec.rb
+++ b/spec/lib/gitlab/request_forgery_protection_spec.rb
@@ -13,6 +13,11 @@ RSpec.describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do
}
end
+ before do
+ allow(env['rack.session']).to receive(:enabled?).and_return(true)
+ allow(env['rack.session']).to receive(:loaded?).and_return(true)
+ end
+
it 'logs to /dev/null' do
expect(ActiveSupport::Logger).to receive(:new).with(File::NULL)
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index fa0fad65520..01cfa91bb59 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
it_behaves_like "valid runtime", :puma, 3 + Gitlab::ActionCable::Config.worker_pool_size
it 'identifies as an application runtime' do
- expect(Gitlab::Runtime.application?).to be true
+ expect(described_class.application?).to be true
end
context "when ActionCable worker pool size is configured" do
@@ -133,7 +133,7 @@ RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
it_behaves_like "valid runtime", :sidekiq, 5
it 'identifies as an application runtime' do
- expect(Gitlab::Runtime.application?).to be true
+ expect(described_class.application?).to be true
end
end
@@ -145,7 +145,7 @@ RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
it_behaves_like "valid runtime", :console, 1
it 'does not identify as an application runtime' do
- expect(Gitlab::Runtime.application?).to be false
+ expect(described_class.application?).to be false
end
end
@@ -157,7 +157,7 @@ RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
it_behaves_like "valid runtime", :test_suite, 1
it 'does not identify as an application runtime' do
- expect(Gitlab::Runtime.application?).to be false
+ expect(described_class.application?).to be false
end
end
@@ -177,7 +177,7 @@ RSpec.describe Gitlab::Runtime, feature_category: :application_performance do
it_behaves_like "valid runtime", :rails_runner, 1
it 'does not identify as an application runtime' do
- expect(Gitlab::Runtime.application?).to be false
+ expect(described_class.application?).to be false
end
end
end
diff --git a/spec/lib/gitlab/search/found_wiki_page_spec.rb b/spec/lib/gitlab/search/found_wiki_page_spec.rb
index fc166ad3851..b84a27fa2cf 100644
--- a/spec/lib/gitlab/search/found_wiki_page_spec.rb
+++ b/spec/lib/gitlab/search/found_wiki_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Search::FoundWikiPage do
+RSpec.describe Gitlab::Search::FoundWikiPage, feature_category: :global_search do
let(:project) { create(:project, :public, :repository) }
describe 'policy' do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index ce54f853e1b..662eab11cc0 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -302,18 +302,6 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
results.objects('users')
end
-
- context 'when autocomplete_users_use_search_service feature flag is disabled' do
- before do
- stub_feature_flags(autocomplete_users_use_search_service: false)
- end
-
- it 'calls the UsersFinder without use_minimum_char_limit' do
- expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original
-
- results.objects('users')
- end
- end
end
end
diff --git a/spec/lib/gitlab/seeder_spec.rb b/spec/lib/gitlab/seeder_spec.rb
index a94ae2bca7a..e5453d338a0 100644
--- a/spec/lib/gitlab/seeder_spec.rb
+++ b/spec/lib/gitlab/seeder_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Seeder do
subject { described_class }
it 'has not_mass_generated scope' do
- expect { Namespace.not_mass_generated }.to raise_error(NoMethodError)
+ expect { described_class.not_mass_generated }.to raise_error(NoMethodError)
Gitlab::Seeder.quiet do
expect { Namespace.not_mass_generated }.not_to raise_error
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 1c23a619b38..4e46a26e89f 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -426,7 +426,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
context 'when the job is deferred' do
- it 'logs start and end of job with deferred job_status' do
+ it 'logs start and end of job with "deferred" job_status' do
travel_to(timestamp) do
expect(logger).to receive(:info).with(start_payload).ordered
expect(logger).to receive(:info).with(deferred_payload).ordered
@@ -436,10 +436,48 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
call_subject(job, 'test_queue') do
job['deferred'] = true
job['deferred_by'] = :feature_flag
+ job['deferred_count'] = 1
end
end
end
end
+
+ context 'when the job is dropped' do
+ it 'logs start and end of job with "dropped" job_status' do
+ travel_to(timestamp) do
+ expect(logger).to receive(:info).with(start_payload).ordered
+ expect(logger).to receive(:info).with(dropped_payload).ordered
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ call_subject(job, 'test_queue') do
+ job['dropped'] = true
+ end
+ end
+ end
+ end
+
+ context 'with a real worker' do
+ let(:worker_class) { AuthorizedKeysWorker.name }
+
+ let(:expected_end_payload) do
+ end_payload.merge(
+ 'urgency' => 'high',
+ 'target_duration_s' => 10
+ )
+ end
+
+ it 'logs job done with urgency and target_duration_s fields' do
+ travel_to(timestamp) do
+ expect(logger).to receive(:info).with(start_payload).ordered
+ expect(logger).to receive(:info).with(expected_end_payload).ordered
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ call_subject(job, 'test_queue') {}
+ end
+ end
+ end
end
describe '#add_time_keys!' do
diff --git a/spec/lib/gitlab/sidekiq_middleware/defer_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/defer_jobs_spec.rb
deleted file mode 100644
index 195a79c22ec..00000000000
--- a/spec/lib/gitlab/sidekiq_middleware/defer_jobs_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::SidekiqMiddleware::DeferJobs, feature_category: :scalability do
- let(:job) { { 'jid' => 123, 'args' => [456] } }
- let(:queue) { 'test_queue' }
- let(:deferred_worker) do
- Class.new do
- def self.name
- 'TestDeferredWorker'
- end
- include ApplicationWorker
- end
- end
-
- let(:undeferred_worker) do
- Class.new do
- def self.name
- 'UndeferredWorker'
- end
- include ApplicationWorker
- end
- end
-
- subject { described_class.new }
-
- before do
- stub_const('TestDeferredWorker', deferred_worker)
- stub_const('UndeferredWorker', undeferred_worker)
- end
-
- describe '#call' do
- context 'with worker not opted for database health check' do
- context 'when sidekiq_defer_jobs feature flag is enabled for a worker' do
- before do
- stub_feature_flags("defer_sidekiq_jobs_#{TestDeferredWorker.name}": true)
- stub_feature_flags("defer_sidekiq_jobs_#{UndeferredWorker.name}": false)
- end
-
- context 'for the affected worker' do
- it 'defers the job' do
- expect(TestDeferredWorker).to receive(:perform_in).with(described_class::DELAY, *job['args'])
- expect { |b| subject.call(TestDeferredWorker.new, job, queue, &b) }.not_to yield_control
- end
- end
-
- context 'for other workers' do
- it 'runs the job normally' do
- expect { |b| subject.call(UndeferredWorker.new, job, queue, &b) }.to yield_control
- end
- end
-
- it 'increments the counter' do
- subject.call(TestDeferredWorker.new, job, queue)
-
- counter = ::Gitlab::Metrics.registry.get(:sidekiq_jobs_deferred_total)
- expect(counter.get({ worker: "TestDeferredWorker" })).to eq(1)
- end
- end
-
- context 'when sidekiq_defer_jobs feature flag is disabled' do
- before do
- stub_feature_flags("defer_sidekiq_jobs_#{TestDeferredWorker.name}": false)
- stub_feature_flags("defer_sidekiq_jobs_#{UndeferredWorker.name}": false)
- end
-
- it 'runs the job normally' do
- expect { |b| subject.call(TestDeferredWorker.new, job, queue, &b) }.to yield_control
- expect { |b| subject.call(UndeferredWorker.new, job, queue, &b) }.to yield_control
- end
- end
- end
-
- context 'with worker opted for database health check' do
- let(:health_signal_attrs) { { gitlab_schema: :gitlab_main, delay: 1.minute, tables: [:users] } }
-
- around do |example|
- with_sidekiq_server_middleware do |chain|
- chain.add described_class
- Sidekiq::Testing.inline! { example.run }
- end
- end
-
- before do
- stub_feature_flags("defer_sidekiq_jobs_#{TestDeferredWorker.name}": false)
-
- TestDeferredWorker.defer_on_database_health_signal(*health_signal_attrs.values)
- end
-
- context 'without any stop signal from database health check' do
- it 'runs the job normally' do
- expect { |b| subject.call(TestDeferredWorker.new, job, queue, &b) }.to yield_control
- end
- end
-
- context 'with stop signal from database health check' do
- before do
- stop_signal = instance_double("Gitlab::Database::HealthStatus::Signals::Stop", stop?: true)
- allow(Gitlab::Database::HealthStatus).to receive(:evaluate).and_return([stop_signal])
- end
-
- it 'defers the job by set time' do
- expect(TestDeferredWorker).to receive(:perform_in).with(health_signal_attrs[:delay], *job['args'])
-
- TestDeferredWorker.perform_async(*job['args'])
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index a46275d90b6..c22e7a1240f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -226,6 +226,14 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
end
+ it 'does not try to set an invalid ttl at the end of expiry' do
+ with_redis { |r| r.expire(cookie_key, 1) }
+
+ sleep 0.5 # sleep 500ms to redis would round the remaining ttl to 0
+
+ expect { subject }.not_to raise_error
+ end
+
context 'and low offsets' do
let(:existing_cookie) do
{
@@ -241,6 +249,24 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
expect(cookie['offsets']).to eq({ 'c1' => 1, 'c2' => 2, 'c3' => 3 })
end
end
+
+ context 'when a WAL location is nil with existing offsets' do
+ let(:existing_cookie) do
+ {
+ 'offsets' => { 'main' => 8, 'ci' => 5 },
+ 'wal_locations' => { 'main' => 'loc1old', 'ci' => 'loc2old' }
+ }
+ end
+
+ let(:argv) { ['main', 9, 'loc1', 'ci', nil, 'loc2'] }
+
+ it 'only updates the main connection' do
+ subject
+
+ expect(cookie['wal_locations']).to eq({ 'main' => 'loc1', 'ci' => 'loc2old' })
+ expect(cookie['offsets']).to eq({ 'main' => 9, 'ci' => 5 })
+ end
+ 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 f04ada688d5..bc69f232d9e 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -59,25 +59,28 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
described_class.initialize_process_metrics
end
- context 'when sidekiq_execution_application_slis FF is turned on' do
- it 'initializes sidekiq SLIs for the workers in the current Sidekiq process' do
+ shared_examples "initializes sidekiq SLIs for the workers in the current process" do
+ before do
allow(Gitlab::SidekiqConfig)
.to receive(:current_worker_queue_mappings)
.and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default')
-
allow(completion_seconds_metric).to receive(:get)
+ end
+ it "initializes the SLIs with labels" do
expect(Gitlab::Metrics::SidekiqSlis)
- .to receive(:initialize_slis!).with([
+ .to receive(initialize_sli_method).with([
{
worker: 'MergeWorker',
urgency: 'high',
- feature_category: 'source_code_management'
+ feature_category: 'source_code_management',
+ external_dependencies: 'no'
},
{
worker: 'Ci::BuildFinishedWorker',
urgency: 'high',
- feature_category: 'continuous_integration'
+ feature_category: 'continuous_integration',
+ external_dependencies: 'no'
}
])
@@ -85,19 +88,47 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
end
end
- context 'when sidekiq_execution_application_slis FF is turned off' do
- before do
- stub_feature_flags(sidekiq_execution_application_slis: false)
- end
-
+ shared_examples "not initializing sidekiq SLIs" do
it 'does not initialize sidekiq SLIs' do
expect(Gitlab::Metrics::SidekiqSlis)
- .not_to receive(:initialize_slis!)
+ .not_to receive(initialize_sli_method)
described_class.initialize_process_metrics
end
end
+ context 'initializing execution SLIs' do
+ let(:initialize_sli_method) { :initialize_execution_slis! }
+
+ context 'when sidekiq_execution_application_slis FF is turned on' do
+ it_behaves_like "initializes sidekiq SLIs for the workers in the current process"
+ end
+
+ context 'when sidekiq_execution_application_slis FF is turned off' do
+ before do
+ stub_feature_flags(sidekiq_execution_application_slis: false)
+ end
+
+ it_behaves_like "not initializing sidekiq SLIs"
+ end
+ end
+
+ context 'initializing queueing SLIs' do
+ let(:initialize_sli_method) { :initialize_queueing_slis! }
+
+ context 'when sidekiq_queueing_application_slis FF is turned on' do
+ it_behaves_like "initializes sidekiq SLIs for the workers in the current process"
+ end
+
+ context 'when sidekiq_queueing_application_slis FF is turned off' do
+ before do
+ stub_feature_flags(sidekiq_queueing_application_slis: false)
+ end
+
+ it_behaves_like "not initializing sidekiq SLIs"
+ end
+ end
+
context 'when the sidekiq_job_completion_metric_initialize feature flag is disabled' do
before do
stub_feature_flags(sidekiq_job_completion_metric_initialize: false)
@@ -119,15 +150,16 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
described_class.initialize_process_metrics
end
- it 'does not initializes sidekiq SLIs' do
- allow(Gitlab::SidekiqConfig)
- .to receive(:current_worker_queue_mappings)
- .and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default')
+ context 'sidekiq execution SLIs' do
+ let(:initialize_sli_method) { :initialize_execution_slis! }
- expect(Gitlab::Metrics::SidekiqSlis)
- .not_to receive(:initialize_slis!)
+ it_behaves_like 'not initializing sidekiq SLIs'
+ end
- described_class.initialize_process_metrics
+ context 'sidekiq queueing SLIs' do
+ let(:initialize_sli_method) { :initialize_queueing_slis! }
+
+ it_behaves_like 'not initializing sidekiq SLIs'
end
end
end
@@ -162,10 +194,19 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
expect(sidekiq_mem_total_bytes).to receive(:set).with(labels_with_job_status, mem_total_bytes)
expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_apdex).with(labels.slice(:worker,
:feature_category,
- :urgency), monotonic_time_duration)
+ :urgency,
+ :external_dependencies), monotonic_time_duration)
expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error).with(labels.slice(:worker,
:feature_category,
- :urgency), false)
+ :urgency,
+ :external_dependencies), false)
+
+ if queue_duration_for_job
+ expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_queueing_apdex).with(labels.slice(:worker,
+ :feature_category,
+ :urgency,
+ :external_dependencies), queue_duration_for_job)
+ end
subject.call(worker, job, :test) { nil }
end
@@ -221,7 +262,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_apdex)
expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error).with(labels.slice(:worker,
:feature_category,
- :urgency), true)
+ :urgency,
+ :external_dependencies), true)
expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
end
@@ -259,6 +301,18 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
subject.call(worker, job, :test) { nil }
end
end
+
+ context 'when sidekiq_queueing_application_slis FF is turned off' do
+ before do
+ stub_feature_flags(sidekiq_queueing_application_slis: false)
+ end
+
+ it 'does not call record_queueing_apdex' do
+ expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_queueing_apdex)
+
+ subject.call(worker, job, :test) { nil }
+ end
+ end
end
end
@@ -400,7 +454,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
Gitlab::SidekiqMiddleware.server_configurator(
metrics: true,
arguments_logger: false,
- defer_jobs: false
+ skip_jobs: false
).call(chain)
Sidekiq::Testing.inline! { example.run }
diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/client_spec.rb
index df8e47d60f0..e1ee12e309f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/size_limiter/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/client_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Client, :clean_gitlab_red
context 'when the validator validates the job suscessfully' do
before do
# Do nothing
- allow(Gitlab::SidekiqMiddleware::SizeLimiter::Client).to receive(:validate!)
+ allow(described_class).to receive(:validate!)
end
it 'raises an exception when scheduling job with #perform_at' do
diff --git a/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
new file mode 100644
index 00000000000..4be21591a40
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/skip_jobs_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqMiddleware::SkipJobs, feature_category: :scalability do
+ let(:job) { { 'jid' => 123, 'args' => [456] } }
+ let(:queue) { 'test_queue' }
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'TestWorker'
+ end
+ include ApplicationWorker
+ end
+ end
+
+ subject { described_class.new }
+
+ before do
+ stub_const('TestWorker', worker)
+ stub_feature_flags("drop_sidekiq_jobs_#{TestWorker.name}": false)
+ end
+
+ describe '#call' do
+ context 'with worker not opted for database health check' do
+ describe "with all combinations of drop and defer FFs" do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:metric) { instance_double(Prometheus::Client::Counter, increment: true) }
+
+ shared_examples 'runs the job normally' do
+ it 'yields control' do
+ expect { |b| subject.call(TestWorker.new, job, queue, &b) }.to yield_control
+ end
+
+ it 'does not increment any metric counter' do
+ expect(metric).not_to receive(:increment)
+
+ subject.call(TestWorker.new, job, queue) { nil }
+ end
+
+ it 'does not increment deferred_count' do
+ subject.call(TestWorker.new, job, queue) { nil }
+
+ expect(job).not_to include('deferred_count')
+ end
+ end
+
+ shared_examples 'drops the job' do
+ it 'does not yield control' do
+ expect { |b| subject.call(TestWorker.new, job, queue, &b) }.not_to yield_control
+ end
+
+ it 'increments counter' do
+ expect(metric).to receive(:increment).with({ worker: "TestWorker", action: "dropped" })
+
+ subject.call(TestWorker.new, job, queue) { nil }
+ end
+
+ it 'does not increment deferred_count' do
+ subject.call(TestWorker.new, job, queue) { nil }
+
+ expect(job).not_to include('deferred_count')
+ end
+
+ it 'has dropped field in job equal to true' do
+ subject.call(TestWorker.new, job, queue) { nil }
+
+ expect(job).to include({ 'dropped' => true })
+ end
+ end
+
+ shared_examples 'defers the job' do
+ it 'does not yield control' do
+ expect { |b| subject.call(TestWorker.new, job, queue, &b) }.not_to yield_control
+ end
+
+ it 'delays the job' do
+ expect(TestWorker).to receive(:perform_in).with(described_class::DELAY, *job['args'])
+
+ subject.call(TestWorker.new, job, queue) { nil }
+ end
+
+ it 'increments counter' do
+ expect(metric).to receive(:increment).with({ worker: "TestWorker", action: "deferred" })
+
+ subject.call(TestWorker.new, job, queue) { nil }
+ end
+
+ it 'has deferred related fields in job payload' do
+ subject.call(TestWorker.new, job, queue) { nil }
+
+ expect(job).to include({ 'deferred' => true, 'deferred_by' => :feature_flag, 'deferred_count' => 1 })
+ end
+ end
+
+ before do
+ stub_feature_flags("drop_sidekiq_jobs_#{TestWorker.name}": drop_ff)
+ stub_feature_flags("run_sidekiq_jobs_#{TestWorker.name}": run_ff)
+ allow(Gitlab::Metrics).to receive(:counter).and_call_original
+ allow(Gitlab::Metrics).to receive(:counter).with(described_class::COUNTER, anything).and_return(metric)
+ end
+
+ where(:drop_ff, :run_ff, :resulting_behavior) do
+ false | true | "runs the job normally"
+ true | true | "drops the job"
+ false | false | "defers the job"
+ true | false | "drops the job"
+ end
+
+ with_them do
+ it_behaves_like params[:resulting_behavior]
+ end
+ end
+ end
+
+ context 'with worker opted for database health check' do
+ let(:health_signal_attrs) { { gitlab_schema: :gitlab_main, delay: 1.minute, tables: [:users] } }
+
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ chain.add described_class
+ Sidekiq::Testing.inline! { example.run }
+ end
+ end
+
+ before do
+ TestWorker.defer_on_database_health_signal(*health_signal_attrs.values)
+ end
+
+ context 'without any stop signal from database health check' do
+ it 'runs the job normally' do
+ expect { |b| subject.call(TestWorker.new, job, queue, &b) }.to yield_control
+ end
+ end
+
+ context 'with stop signal from database health check' do
+ before do
+ stop_signal = instance_double("Gitlab::Database::HealthStatus::Signals::Stop", stop?: true)
+ allow(Gitlab::Database::HealthStatus).to receive(:evaluate).and_return([stop_signal])
+ end
+
+ it 'defers the job by set time' do
+ expect(TestWorker).to receive(:perform_in).with(health_signal_attrs[:delay], *job['args'])
+
+ TestWorker.perform_async(*job['args'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index 7e53b6598b6..5a38d1b7750 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do
shared_examples "a middleware chain" do
before do
configurator.call(chain)
- stub_feature_flags("defer_sidekiq_jobs_#{worker_class.name}": false) # not letting this worker deferring its jobs
+ stub_feature_flags("drop_sidekiq_jobs_#{worker_class.name}": false) # not dropping the job
end
it "passes through the right middlewares", :aggregate_failures do
enabled_sidekiq_middlewares.each do |middleware|
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do
::Gitlab::SidekiqMiddleware::WorkerContext::Server,
::Gitlab::SidekiqMiddleware::DuplicateJobs::Server,
::Gitlab::Database::LoadBalancing::SidekiqServerMiddleware,
- ::Gitlab::SidekiqMiddleware::DeferJobs
+ ::Gitlab::SidekiqMiddleware::SkipJobs
]
end
@@ -80,9 +80,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do
described_class.server_configurator(
metrics: true,
arguments_logger: true,
- # defer_jobs has to be false because this middleware defers jobs from a worker based on
- # `worker` type feature flag which is enabled by default in test
- defer_jobs: false
+ skip_jobs: false
).call(chain)
Sidekiq::Testing.inline! { example.run }
@@ -115,7 +113,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do
described_class.server_configurator(
metrics: false,
arguments_logger: false,
- defer_jobs: false
+ skip_jobs: false
)
end
@@ -123,7 +121,7 @@ RSpec.describe Gitlab::SidekiqMiddleware do
[
Gitlab::SidekiqMiddleware::ServerMetrics,
Gitlab::SidekiqMiddleware::ArgumentsLogger,
- Gitlab::SidekiqMiddleware::DeferJobs
+ Gitlab::SidekiqMiddleware::SkipJobs
]
end
diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
index 5d62e96971b..3af0ae03256 100644
--- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
+++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::SlashCommands::Presenters::Access do
it { is_expected.to be_a(Hash) }
it_behaves_like 'displays an error message' do
- let(:error_message) { 'your account has been deactivated by your administrator' }
+ let(:error_message) { "your #{Gitlab.config.gitlab.url} account needs to be reactivated" }
end
end
diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb
index 080c2803ddd..354f25c0b01 100644
--- a/spec/lib/gitlab/spamcheck/client_spec.rb
+++ b/spec/lib/gitlab/spamcheck/client_spec.rb
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Spamcheck::Client, feature_category: :instance_resiliency
before do
allow(generic_spammable).to receive_messages(
- spammable_entity_type: 'generic',
+ to_ability_name: 'generic_spammable',
spammable_text: 'generic spam',
created_at: generic_created_at,
updated_at: generic_updated_at,
@@ -152,7 +152,7 @@ RSpec.describe Gitlab::Spamcheck::Client, feature_category: :instance_resiliency
generic_pb, _ = described_class.new.send(:build_protobuf, spammable: generic_spammable, user: user, context: cxt, extra_features: {})
expect(generic_pb.text).to eq 'generic spam'
- expect(generic_pb.type).to eq 'generic'
+ expect(generic_pb.type).to eq 'generic_spammable'
expect(generic_pb.created_at).to eq timestamp_to_protobuf_timestamp(generic_created_at)
expect(generic_pb.updated_at).to eq timestamp_to_protobuf_timestamp(generic_updated_at)
expect(generic_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
diff --git a/spec/lib/gitlab/ssh/commit_spec.rb b/spec/lib/gitlab/ssh/commit_spec.rb
index 77f37857c82..3b53ed9d1db 100644
--- a/spec/lib/gitlab/ssh/commit_spec.rb
+++ b/spec/lib/gitlab/ssh/commit_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe Gitlab::Ssh::Commit, feature_category: :source_code_management do
let(:commit) { create(:commit, project: project) }
let(:signature_text) { 'signature_text' }
let(:signed_text) { 'signed_text' }
- let(:signature_data) { [signature_text, signed_text] }
+ let(:signer) { :SIGNER_USER }
+ let(:signature_data) { { signature: signature_text, signed_text: signed_text, signer: signer } }
let(:verifier) { instance_double('Gitlab::Ssh::Signature') }
let(:verification_status) { :verified }
@@ -27,7 +28,7 @@ RSpec.describe Gitlab::Ssh::Commit, feature_category: :source_code_management do
})
allow(Gitlab::Ssh::Signature).to receive(:new)
- .with(signature_text, signed_text, commit.committer_email)
+ .with(signature_text, signed_text, signer, commit.committer_email)
.and_return(verifier)
end
diff --git a/spec/lib/gitlab/ssh/signature_spec.rb b/spec/lib/gitlab/ssh/signature_spec.rb
index ee9b38cae7d..cb0b1ff049c 100644
--- a/spec/lib/gitlab/ssh/signature_spec.rb
+++ b/spec/lib/gitlab/ssh/signature_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ssh::Signature, feature_category: :source_code_management
let_it_be_with_reload(:key) { create(:key, usage_type: :signing, key: public_key_text, user: user) }
let(:signed_text) { 'This message was signed by an ssh key' }
+ let(:signer) { :SIGNER_USER }
let(:signature_text) do
# ssh-keygen -Y sign -n git -f id_test message.txt
@@ -27,6 +28,7 @@ RSpec.describe Gitlab::Ssh::Signature, feature_category: :source_code_management
described_class.new(
signature_text,
signed_text,
+ signer,
committer_email
)
end
@@ -266,6 +268,15 @@ RSpec.describe Gitlab::Ssh::Signature, feature_category: :source_code_management
expect(signature.verification_status).to eq(:other_user)
end
end
+
+ context 'when signature created by GitLab' do
+ let(:signer) { :SIGNER_SYSTEM }
+
+ it 'reports verified_system status' do
+ expect(signature.verification_status).to eq(:verified_system)
+ expect(signature.key_fingerprint).to eq('dw7gPSvYtkCBU+BbTolbbckUEX3sL6NsGIJTQ4PYEnM')
+ end
+ end
end
describe '#key_fingerprint' do
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index c02cbef8328..5f76c1de5b1 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::UrlSanitizer do
describe '.sanitize' do
def sanitize_url(url)
# We want to try with multi-line content because is how error messages are formatted
- described_class.sanitize(%Q{
+ described_class.sanitize(%{
remote: Not Found
fatal: repository `#{url}` not found
})
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index c336a4850d2..d67bb477350 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -178,6 +178,45 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
end
end
+ describe '#events' do
+ context 'when metric is not event based' do
+ it 'returns empty hash' do
+ expect(definition.events).to eq({})
+ end
+ end
+
+ context 'when metric is using old format' do
+ let(:attributes) { { options: { events: ['my_event'] } } }
+
+ it 'returns a correct hash' do
+ expect(definition.events).to eq({ 'my_event' => nil })
+ end
+ end
+
+ context 'when metric is using new format' do
+ let(:attributes) { { events: [{ name: 'my_event', unique: 'user_id' }] } }
+
+ it 'returns a correct hash' do
+ expect(definition.events).to eq({ 'my_event' => :user_id })
+ end
+ end
+
+ context 'when metric is using both formats' do
+ let(:attributes) do
+ {
+ options: {
+ events: ['a_event']
+ },
+ events: [{ name: 'my_event', unique: 'project_id' }]
+ }
+ end
+
+ it 'uses the new format' do
+ expect(definition.events).to eq({ 'my_event' => :project_id })
+ end
+ end
+ end
+
describe '#valid_service_ping_status?' do
context 'when metric has active status' do
it 'has to return true' do
@@ -305,4 +344,71 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml)
end
end
+
+ describe '.metric_definitions_changed?', :freeze_time do
+ let(:metric1) { Dir.mktmpdir('metric1') }
+ let(:metric2) { Dir.mktmpdir('metric2') }
+
+ before do
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(is_dev)
+ allow(described_class).to receive(:paths).and_return(
+ [
+ File.join(metric1, '**', '*.yml'),
+ File.join(metric2, '**', '*.yml')
+ ]
+ )
+
+ write_metric(metric1, path, yaml_content)
+ write_metric(metric2, path, yaml_content)
+ end
+
+ after do
+ FileUtils.rm_rf(metric1)
+ FileUtils.rm_rf(metric2)
+ end
+
+ context 'in development', :freeze_time do
+ let(:is_dev) { true }
+
+ it 'has changes on the first invocation' do
+ expect(described_class.metric_definitions_changed?).to be_truthy
+ end
+
+ context 'when no files are changed' do
+ it 'does not have changes on the second invocation' do
+ described_class.metric_definitions_changed?
+
+ expect(described_class.metric_definitions_changed?).to be_falsy
+ end
+ end
+
+ context 'when file is changed' do
+ it 'has changes on the next invocation when more than 3 seconds have passed' do
+ described_class.metric_definitions_changed?
+
+ write_metric(metric1, path, yaml_content)
+ travel_to 10.seconds.from_now
+
+ expect(described_class.metric_definitions_changed?).to be_truthy
+ end
+
+ it 'does not have changes on the next invocation when less than 3 seconds have passed' do
+ described_class.metric_definitions_changed?
+
+ write_metric(metric1, path, yaml_content)
+ travel_to 1.second.from_now
+
+ expect(described_class.metric_definitions_changed?).to be_falsy
+ end
+ end
+
+ context 'in production' do
+ let(:is_dev) { false }
+
+ it 'does not detect changes' do
+ expect(described_class.metric_definitions_changed?).to be_falsy
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
index 8be0769a379..c3060cd4927 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -28,6 +28,10 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
7 | 'OR' | 'redis_hll' | :calculate_metrics_union
28 | 'OR' | 'database' | :calculate_metrics_union
7 | 'OR' | 'database' | :calculate_metrics_union
+ 28 | 'AND' | 'internal_events' | :calculate_metrics_intersections
+ 7 | 'AND' | 'internal_events' | :calculate_metrics_intersections
+ 28 | 'OR' | 'internal_events' | :calculate_metrics_union
+ 7 | 'OR' | 'internal_events' | :calculate_metrics_union
end
with_them do
@@ -152,6 +156,7 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
where(:time_frame, :operator, :datasource) do
'28d' | 'OR' | 'redis_hll'
'7d' | 'OR' | 'database'
+ '28d' | 'OR' | 'internal_events'
end
with_them do
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
index 83b155b41b1..a137a97a978 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do
.and_return(5)
end
- expect(Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll).to receive(:calculate_metrics_union)
+ expect(described_class).to receive(:calculate_metrics_union)
.with(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
.and_return(2)
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do
end
it 'raises error if union is < 0' do
- allow(Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll).to receive(:calculate_metrics_union).and_raise(Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable)
+ allow(described_class).to receive(:calculate_metrics_union).and_raise(Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable)
expect { calculate_metrics_intersections }.to raise_error(Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable)
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migrations_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migrations_metric_spec.rb
new file mode 100644
index 00000000000..f7b15539400
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migrations_metric_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::BatchedBackgroundMigrationsMetric, feature_category: :database do
+ let(:expected_value) do
+ [
+ {
+ job_class_name: 'test',
+ elapsed_time: 2.days.to_i
+ }
+ ]
+ end
+
+ let_it_be(:active_migration) { create(:batched_background_migration, :active) }
+ let_it_be(:finished_migration) do
+ create(:batched_background_migration, :finished, job_class_name: 'test', started_at: 5.days.ago,
+ finished_at: 3.days.ago)
+ end
+
+ let_it_be(:old_finished_migration) do
+ create(:batched_background_migration, :finished, job_class_name: 'old_test', started_at: 100.days.ago,
+ finished_at: 99.days.ago)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '7d' }
+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 317929f77e6..eee5396bdbf 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
@@ -30,8 +30,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
context 'for 28d time frame' do
let(:expected_value) { 6 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
+ let(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}'"
@@ -63,8 +63,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
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(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}'"\
@@ -92,8 +92,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
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(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}'"\
@@ -121,8 +121,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
context 'for 28d time frame' do
let(:expected_value) { 4 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
+ let(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}'"\
@@ -150,8 +150,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
context 'for 28d time frame' do
let(:expected_value) { 2 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
+ let(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}'"\
@@ -202,8 +202,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
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(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}' " \
@@ -249,8 +249,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
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(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}' " \
@@ -265,8 +265,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
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(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(: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}' " \
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb
index b7da9b27e19..8ae64e8db23 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_metric_spec.rb
@@ -43,8 +43,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountImportedProjectsMe
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(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(:db) }
let(:expected_query) do
"SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"created_at\""\
" BETWEEN '#{start}' AND '#{finish}' AND \"projects\".\"import_type\" = 'gitea'"
@@ -70,8 +70,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountImportedProjectsMe
context 'for 28d time frame' do
let(:expected_value) { 2 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
+ let(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(:db) }
let(:expected_query) do
"SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"created_at\""\
" BETWEEN '#{start}' AND '#{finish}' AND \"projects\".\"import_type\" = 'bitbucket'"
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb
index bfc4240def6..bd432b614e7 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_imported_projects_total_metric_spec.rb
@@ -45,8 +45,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountImportedProjectsTo
context 'for 28d time frame' do
let(:expected_value) { 8 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
+ let(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(:db) }
let(:expected_query) do
"SELECT (SELECT COUNT(\"projects\".\"id\") FROM \"projects\" WHERE \"projects\".\"import_type\""\
" IN ('gitlab_project', 'gitlab', 'github', 'bitbucket', 'bitbucket_server', 'gitea', 'git', 'manifest',"\
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
new file mode 100644
index 00000000000..a2d86fc5044
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_jira_dvcs_integration_metric_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithJiraDvcsIntegrationMetric,
+ feature_category: :integrations do
+ describe 'metric value and query' do
+ let_it_be_with_reload(:project_1) { create(:project) }
+ let_it_be_with_reload(:project_2) { create(:project) }
+ let_it_be_with_reload(:project_3) { create(:project) }
+
+ before do
+ project_1.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
+ project_2.feature_usage.log_jira_dvcs_integration_usage(cloud: false)
+ project_3.feature_usage.log_jira_dvcs_integration_usage(cloud: true)
+ end
+
+ context 'when counting cloud integrations' do
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
+ 'WHERE "project_feature_usages"."jira_dvcs_cloud_last_sync_at" IS NOT NULL'
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: true } }
+ end
+
+ context 'when counting non-cloud integrations' do
+ let(:expected_value) { 2 }
+ let(:expected_query) do
+ 'SELECT COUNT("project_feature_usages"."project_id") FROM "project_feature_usages" ' \
+ 'WHERE "project_feature_usages"."jira_dvcs_server_last_sync_at" IS NOT NULL'
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', options: { cloud: false } }
+ end
+ end
+
+ it "raises an exception if option is not present" do
+ expect do
+ described_class.new(options: {}, time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{must be a boolean})
+ end
+
+ it "raises an exception if option has invalid value" do
+ expect do
+ described_class.new(options: { cloud: 'yes' }, time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{must be a boolean})
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_gbp_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_gbp_metric_spec.rb
new file mode 100644
index 00000000000..aa14ffed25b
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_gbp_metric_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountSlackAppInstallationsGbpMetric, feature_category: :integrations do
+ let_it_be(:slack_integration) { create(:slack_integration) }
+ let_it_be(:slack_integration_legacy) { create(:slack_integration, :legacy) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT("slack_integrations"."id") FROM "slack_integrations" ' \
+ 'WHERE "slack_integrations"."bot_user_id" IS NOT NULL'
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_metric_spec.rb
new file mode 100644
index 00000000000..cfbecdad468
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_metric_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountSlackAppInstallationsMetric, feature_category: :integrations do
+ let_it_be(:slack_integration) { create(:slack_integration) }
+ let_it_be(:slack_integration_legacy) { create(:slack_integration, :legacy) }
+
+ let(:expected_value) { 2 }
+ let(:expected_query) { 'SELECT COUNT("slack_integrations"."id") FROM "slack_integrations"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
index 3fb4c3a4e3f..86aa37b494a 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
@@ -16,8 +16,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssue
context 'for 28d time frame' do
let(:expected_value) { 1 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
+ let(:start) { 30.days.ago.to_fs(:db) }
+ let(:finish) { 2.days.ago.to_fs(:db) }
let(:expected_query) { "SELECT COUNT(DISTINCT \"issues\".\"author_id\") FROM \"issues\" WHERE \"issues\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'" }
it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/gitaly_apdex_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/gitaly_apdex_metric_spec.rb
new file mode 100644
index 00000000000..fc1e546ef8b
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/gitaly_apdex_metric_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GitalyApdexMetric, feature_category: :service_ping do
+ let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) }
+ let(:metric) { described_class.new(time_frame: 'none') }
+
+ before do
+ allow(prometheus_client).to receive(:query)
+ .with(/gitlab_usage_ping:gitaly_apdex:ratio_avg_over_time_5m/)
+ .and_return(
+ [
+ { 'metric' => {},
+ 'value' => [1616016381.473, '0.95'] }
+ ])
+ # rubocop:disable RSpec/AnyInstanceOf
+ allow_any_instance_of(Gitlab::Utils::UsageData).to receive(:with_prometheus_client).and_yield(prometheus_client)
+ # rubocop:enable RSpec/AnyInstanceOf
+ end
+
+ it 'gathers gitaly apdex', :aggregate_failures do
+ expect(metric.value).to be_within(0.001).of(0.95)
+ end
+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
index 92a576d1a9f..d8c5204d3d8 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::IndexInconsistenciesMet
]
end
- let(:runner) { instance_double(Gitlab::Database::SchemaValidation::Runner, execute: inconsistencies) }
- let(:inconsistency_class) { Gitlab::Database::SchemaValidation::Inconsistency }
+ let(:runner) { instance_double(Gitlab::Schema::Validation::Runner, execute: inconsistencies) }
+ let(:inconsistency_class) { Gitlab::Schema::Validation::Inconsistency }
let(:inconsistencies) do
[
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::IndexInconsistenciesMet
end
before do
- allow(Gitlab::Database::SchemaValidation::Runner).to receive(:new).and_return(runner)
+ allow(Gitlab::Schema::Validation::Runner).to receive(:new).and_return(runner)
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/ldap_encrypted_secrets_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/ldap_encrypted_secrets_metric_spec.rb
new file mode 100644
index 00000000000..1775304ad87
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/ldap_encrypted_secrets_metric_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::LdapEncryptedSecretsMetric, feature_category: :service_ping do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:encrypted_config) { instance_double(Gitlab::EncryptedConfiguration) }
+
+ where(:ldap_encrypted_secrets_enabled, :expected_value) do
+ true | true
+ false | false
+ end
+
+ with_them do
+ before do
+ allow(Gitlab::Auth::Ldap::Config).to receive(:encrypted_secrets).and_return(encrypted_config)
+ allow(encrypted_config).to receive(:active?).and_return(ldap_encrypted_secrets_enabled)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/operating_system_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/operating_system_metric_spec.rb
new file mode 100644
index 00000000000..26f4cfd252e
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/operating_system_metric_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::OperatingSystemMetric, feature_category: :service_ping do
+ let(:ohai_data) { { "platform" => "ubuntu", "platform_version" => "20.04" } }
+ let(:expected_value) { 'ubuntu-20.04' }
+
+ before do
+ allow_next_instance_of(Ohai::System) do |ohai|
+ allow(ohai).to receive(:data).and_return(ohai_data)
+ end
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
+
+ context 'when on Debian with armv architecture' do
+ let(:ohai_data) { { "platform" => "debian", "platform_version" => "10", 'kernel' => { 'machine' => 'armv' } } }
+ let(:expected_value) { 'raspbian-10' }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/schema_inconsistencies_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/schema_inconsistencies_metric_spec.rb
new file mode 100644
index 00000000000..603e68991cc
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/schema_inconsistencies_metric_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::SchemaInconsistenciesMetric, feature_category: :database do
+ before do
+ allow(Gitlab::Schema::Validation::Runner).to receive(:new).and_return(runner)
+ end
+
+ let(:runner) { instance_double(Gitlab::Schema::Validation::Runner, execute: inconsistencies) }
+ let(:inconsistency_class) { Gitlab::Schema::Validation::Inconsistency }
+
+ let(:inconsistencies) do
+ [
+ instance_double(inconsistency_class, object_name: 'index_name_1', type: 'wrong_indexes', object_type: 'index'),
+ instance_double(inconsistency_class, object_name: 'index_name_2', type: 'missing_indexes',
+ object_type: 'index'),
+ instance_double(inconsistency_class, object_name: 'index_name_3', type: 'extra_indexes', object_type: 'index')
+ ]
+ end
+
+ 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', object_type: 'index' },
+ { inconsistency_type: 'missing_indexes', object_name: 'index_name_2', object_type: 'index' },
+ { inconsistency_type: 'extra_indexes', object_name: 'index_name_3', object_type: 'index' }
+ ]
+ end
+ end
+
+ context 'when the max number of inconsistencies is exceeded' do
+ before do
+ stub_const('Gitlab::Usage::Metrics::Instrumentations::SchemaInconsistenciesMetric::MAX_INCONSISTENCIES', 1)
+ end
+
+ 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', object_type: 'index' }]
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/smtp_encrypted_secrets_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/smtp_encrypted_secrets_metric_spec.rb
new file mode 100644
index 00000000000..cf9cec8118f
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/smtp_encrypted_secrets_metric_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::SmtpEncryptedSecretsMetric, feature_category: :service_ping do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:encrypted_config) { instance_double(Gitlab::EncryptedConfiguration) }
+
+ where(:smtp_encrypted_secrets_enabled, :expected_value) do
+ true | true
+ false | false
+ end
+
+ with_them do
+ before do
+ allow(Gitlab::Email::SmtpConfig).to receive(:encrypted_secrets).and_return(encrypted_config)
+ allow(encrypted_config).to receive(:active?).and_return(smtp_encrypted_secrets_enabled)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
index 5002ee7599f..884d73a70f3 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -88,8 +88,8 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator, feature_cate
context 'for alt_usage_data metrics' do
it_behaves_like 'name suggestion' do
- # corresponding metric is collected with alt_usage_data(fallback: nil) { operating_system }
- let(:key_path) { 'settings.operating_system' }
+ # corresponding metric is collected with alt_usage_data { ApplicationRecord.database.version }
+ let(:key_path) { 'database.version' }
let(:name_suggestion) { /<please fill metric name>/ }
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 50fb9f9df6e..fc1d66d1d62 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
@@ -18,28 +18,71 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
# depending on which day of the week test is run.
# Monday 6th of June
described_class.clear_memoization(:known_events)
+ described_class.clear_memoization(:known_events_names)
reference_time = Time.utc(2020, 6, 1)
travel_to(reference_time) { example.run }
described_class.clear_memoization(:known_events)
+ described_class.clear_memoization(:known_events_names)
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) { { "name" => "ce_event" } }
- before do
- stub_const("#{described_class}::KNOWN_EVENTS_PATH", File.expand_path('*.yml', ce_temp_dir))
- File.open(ce_temp_file.path, "w+b") { |f| f.write [ce_event].to_yaml }
- end
+ context 'with use_metric_definitions_for_events_list disabled' do
+ let(:ce_temp_dir) { Dir.mktmpdir }
+ let(:ce_temp_file) { Tempfile.new(%w[common .yml], ce_temp_dir) }
+
+ before do
+ stub_feature_flags(use_metric_definitions_for_events_list: false)
+ stub_const("#{described_class}::KNOWN_EVENTS_PATH", File.expand_path('*.yml', ce_temp_dir))
+ File.open(ce_temp_file.path, "w+b") { |f| f.write [ce_event].to_yaml }
+ end
- after do
- ce_temp_file.unlink
- FileUtils.remove_entry(ce_temp_dir) if Dir.exist?(ce_temp_dir)
+ after do
+ ce_temp_file.unlink
+ FileUtils.remove_entry(ce_temp_dir) if Dir.exist?(ce_temp_dir)
+ end
+
+ it 'returns ce events' do
+ expect(described_class.known_events).to include(ce_event)
+ end
end
- it 'returns ce events' do
- expect(described_class.known_events).to include(ce_event)
+ context 'with use_metric_definitions_for_events_list enabled' do
+ let(:removed_ce_event) { { "name" => "removed_ce_event" } }
+ let(:metric_definition) do
+ Gitlab::Usage::MetricDefinition.new('ce_metric',
+ {
+ key_path: 'ce_metric_weekly',
+ status: 'active',
+ options: {
+ events: [ce_event['name']]
+ }
+ })
+ end
+
+ let(:removed_metric_definition) do
+ Gitlab::Usage::MetricDefinition.new('removed_ce_metric',
+ {
+ key_path: 'removed_ce_metric_weekly',
+ status: 'removed',
+ options: {
+ events: [removed_ce_event['name']]
+ }
+ })
+ end
+
+ before do
+ allow(Gitlab::Usage::MetricDefinition).to receive(:all).and_return([metric_definition, removed_metric_definition])
+ end
+
+ it 'returns ce events' do
+ expect(described_class.known_events).to include(ce_event)
+ end
+
+ it 'does not return removed events' do
+ expect(described_class.known_events).not_to include(removed_ce_event)
+ end
end
end
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 ba83d979cad..50e20e4fbcf 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
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
context 'for Issue created actions' do
- it_behaves_like 'tracked issuable snowplow and service ping events with project' do
+ it_behaves_like 'tracked issuable internal event with project' do
let(:action) { described_class::ISSUE_CREATED }
let(:original_params) { { namespace: project.project_namespace.reload } }
diff --git a/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb
index 42855271e22..9562f1c5500 100644
--- a/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb
@@ -13,7 +13,9 @@ RSpec.describe Gitlab::UsageDataCounters::KubernetesAgentCounter do
{
'gitops_sync' => 1,
'k8s_api_proxy_request' => 2,
- 'flux_git_push_notifications_total' => 3
+ 'flux_git_push_notifications_total' => 3,
+ 'k8s_api_proxy_requests_via_ci_access' => 4,
+ 'k8s_api_proxy_requests_via_user_access' => 5
}
end
@@ -27,7 +29,10 @@ RSpec.describe Gitlab::UsageDataCounters::KubernetesAgentCounter do
expect(described_class.totals).to eq(
kubernetes_agent_gitops_sync: 3,
kubernetes_agent_k8s_api_proxy_request: 6,
- kubernetes_agent_flux_git_push_notifications_total: 9)
+ kubernetes_agent_flux_git_push_notifications_total: 9,
+ kubernetes_agent_k8s_api_proxy_requests_via_ci_access: 12,
+ kubernetes_agent_k8s_api_proxy_requests_via_user_access: 15
+ )
end
context 'with empty events' do
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 25c57aa00c6..53eee62b386 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
@@ -54,11 +54,6 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:merge_request) { create(:merge_request) }
let(:target_project) { merge_request.target_project }
- let(:fake_tracker) { instance_spy(Gitlab::Tracking::Destinations::Snowplow) }
-
- before do
- allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_tracker)
- end
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_USER_CREATE_ACTION }
@@ -68,36 +63,10 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_CREATE_ACTION }
end
- it 'logs to Snowplow', :aggregate_failures do
- # This logic should be extracted to shared_examples
- namespace = target_project.namespace
-
- expect(Gitlab::Tracking::StandardContext)
- .to receive(:new)
- .with(
- project_id: target_project.id,
- user_id: user.id,
- namespace_id: namespace.id,
- plan_name: namespace.actual_plan_name
- )
- .and_call_original
-
- expect(Gitlab::Tracking::ServicePingContext)
- .to receive(:new)
- .with(data_source: :redis_hll, event: described_class::MR_USER_CREATE_ACTION)
- .and_call_original
-
- expect(fake_tracker).to receive(:event)
- .with(
- 'InternalEventTracking',
- described_class::MR_USER_CREATE_ACTION,
- context: [
- an_instance_of(SnowplowTracker::SelfDescribingJson),
- an_instance_of(SnowplowTracker::SelfDescribingJson)
- ]
- )
- .exactly(:once)
- subject
+ it_behaves_like 'internal event tracking' do
+ let(:action) { described_class::MR_USER_CREATE_ACTION }
+ let(:project) { target_project }
+ let(:namespace) { project.namespace }
end
end
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index 1f52819fd9e..06e85a34ec9 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -55,7 +55,11 @@ RSpec.describe Gitlab::UsageDataMetrics, :with_license, feature_category: :servi
let(:metric_files_key_paths) do
Gitlab::Usage::MetricDefinition
.definitions
- .select { |k, v| v.attributes[:data_source] == 'redis_hll' && v.key_path.starts_with?('redis_hll_counters') && v.available? }
+ .select do |_, v|
+ (v.data_source == 'redis_hll' || v.data_source == 'internal_events') &&
+ v.key_path.starts_with?('redis_hll_counters') &&
+ v.available?
+ end
.keys
.sort
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 9df869f8801..94c4544f754 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -20,7 +20,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
is_expected.to include(:counts_monthly)
is_expected.to include(:counts_weekly)
is_expected.to include(:license)
- is_expected.to include(:settings)
# usage_activity_by_stage data
is_expected.to include(:usage_activity_by_stage)
@@ -98,6 +97,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
it 'includes accurate usage_activity_by_stage data' do
for_defined_days_back do
user = create(:user)
+ project = create(:project, creator: user)
create(:cluster, user: user)
create(:cluster, :disabled, user: user)
create(:cluster_provider_gcp, :created)
@@ -108,6 +108,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
create(:cluster, :instance, :disabled, :production_environment)
create(:cluster, :instance, :production_environment)
create(:cluster, :management_project)
+ create(:integrations_slack, project: project)
+ create(:slack_slash_commands_integration, project: project)
+ create(:prometheus_integration, project: project)
end
expect(described_class.usage_activity_by_stage_configure({})).to include(
@@ -122,7 +125,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
group_clusters_disabled: 2,
group_clusters_enabled: 2,
project_clusters_disabled: 2,
- project_clusters_enabled: 10
+ project_clusters_enabled: 10,
+ projects_slack_notifications_active: 2,
+ projects_slack_slash_active: 2
)
expect(described_class.usage_activity_by_stage_configure(described_class.monthly_time_range_db_params)).to include(
clusters_management_project: 1,
@@ -136,7 +141,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
group_clusters_disabled: 1,
group_clusters_enabled: 1,
project_clusters_disabled: 1,
- project_clusters_enabled: 5
+ project_clusters_enabled: 5,
+ projects_slack_notifications_active: 1,
+ projects_slack_slash_active: 1
)
end
end
@@ -804,7 +811,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
let(:project) { create(:project) }
let(:description_with_embed) { "Some comment\n\nhttps://grafana.example.com/d/xvAk4q0Wk/go-processes?orgId=1&from=1573238522762&to=1573240322762&var-job=prometheus&var-interval=10m&panelId=1&fullscreen" }
let(:description_with_unintegrated_embed) { "Some comment\n\nhttps://grafana.exp.com/d/xvAk4q0Wk/go-processes?orgId=1&from=1573238522762&to=1573240322762&var-job=prometheus&var-interval=10m&panelId=1&fullscreen" }
- let(:description_with_non_grafana_inline_metric) { "Some comment\n\n#{Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(*['foo', 'bar', 12])}" }
shared_examples "zero count" do
it "does not count the issue" do
@@ -824,7 +830,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
create(:issue, project: project, description: description_with_embed)
# In-Valid
create(:issue, project: project, description: description_with_unintegrated_embed)
- create(:issue, project: project, description: description_with_non_grafana_inline_metric)
create(:issue, project: project, description: nil)
create(:issue, project: project, description: '')
create(:issue, project: project)
@@ -862,97 +867,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
end
end
end
-
- describe ".operating_system" do
- let(:ohai_data) { { "platform" => "ubuntu", "platform_version" => "20.04" } }
-
- before do
- allow_next_instance_of(Ohai::System) do |ohai|
- allow(ohai).to receive(:data).and_return(ohai_data)
- end
- end
-
- subject { described_class.operating_system }
-
- it { is_expected.to eq("ubuntu-20.04") }
-
- context 'when on Debian with armv architecture' do
- let(:ohai_data) { { "platform" => "debian", "platform_version" => "10", 'kernel' => { 'machine' => 'armv' } } }
-
- it { is_expected.to eq("raspbian-10") }
- end
- end
-
- describe ".system_usage_data_settings" do
- let(:prometheus_client) { double(Gitlab::PrometheusClient) }
- let(:snowplow_gitlab_host?) { Gitlab::CurrentSettings.snowplow_collector_hostname == 'snowplow.trx.gitlab.net' }
-
- before do
- allow(described_class).to receive(:operating_system).and_return('ubuntu-20.04')
- expect(prometheus_client).to receive(:query)
- .with(/gitlab_usage_ping:gitaly_apdex:ratio_avg_over_time_5m/)
- .and_return(
- [
- { 'metric' => {},
- 'value' => [1616016381.473, '0.95'] }
- ])
- expect(described_class).to receive(:with_prometheus_client).and_yield(prometheus_client)
- end
-
- subject { described_class.system_usage_data_settings }
-
- it 'gathers encrypted secrets usage data', :aggregate_failures do
- expect(subject[:settings][:ldap_encrypted_secrets_enabled]).to eq(Gitlab::Auth::Ldap::Config.encrypted_secrets.active?)
- expect(subject[:settings][:smtp_encrypted_secrets_enabled]).to eq(Gitlab::Email::SmtpConfig.encrypted_secrets.active?)
- end
-
- it 'populates operating system information' do
- expect(subject[:settings][:operating_system]).to eq('ubuntu-20.04')
- end
-
- it 'gathers gitaly apdex', :aggregate_failures do
- expect(subject[:settings][:gitaly_apdex]).to be_within(0.001).of(0.95)
- end
-
- it 'reports collected data categories' do
- expected_value = %w[standard subscription operational optional]
-
- allow_next_instance_of(ServicePing::PermitDataCategories) do |instance|
- expect(instance).to receive(:execute).and_return(expected_value)
- end
-
- expect(subject[:settings][:collected_data_categories]).to eq(expected_value)
- end
-
- it 'gathers service_ping_features_enabled' do
- expect(subject[:settings][:service_ping_features_enabled]).to eq(Gitlab::CurrentSettings.usage_ping_features_enabled)
- end
-
- it 'gathers user_cap_feature_enabled' do
- expect(subject[:settings][:user_cap_feature_enabled]).to eq(Gitlab::CurrentSettings.new_user_signups_cap)
- end
-
- it 'reports status of the certificate_based_clusters feature flag as true' do
- expect(subject[:settings][:certificate_based_clusters_ff]).to eq(true)
- end
-
- context 'with certificate_based_clusters disabled' do
- before do
- stub_feature_flags(certificate_based_clusters: false)
- end
-
- it 'reports status of the certificate_based_clusters feature flag as false' do
- expect(subject[:settings][:certificate_based_clusters_ff]).to eq(false)
- end
- end
-
- context 'snowplow stats' do
- it 'gathers snowplow stats' do
- expect(subject[:settings][:snowplow_enabled]).to eq(Gitlab::CurrentSettings.snowplow_enabled?)
- expect(subject[:settings][:snowplow_configured_to_gitlab_collector]).to eq(snowplow_gitlab_host?)
- end
- end
- end
end
def for_defined_days_back(days: [31, 3])
diff --git a/spec/lib/gitlab/utils/measuring_spec.rb b/spec/lib/gitlab/utils/measuring_spec.rb
index 4d2791f771f..da5ef7e08a7 100644
--- a/spec/lib/gitlab/utils/measuring_spec.rb
+++ b/spec/lib/gitlab/utils/measuring_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Utils::Measuring do
let(:result) { "result" }
before do
- allow(ActiveSupport::Logger).to receive(:logger_outputs_to?).with(Gitlab::Utils::Measuring.logger, $stdout).and_return(false)
+ allow(ActiveSupport::Logger).to receive(:logger_outputs_to?).with(described_class.logger, $stdout).and_return(false)
end
let(:measurement) { described_class.new(base_log_data) }
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
deleted file mode 100644
index ea8083e7d7f..00000000000
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ /dev/null
@@ -1,374 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-benchmark'
-require 'rspec-parameterized'
-require 'active_support/testing/time_helpers'
-
-RSpec.configure do |config|
- config.include RSpec::Benchmark::Matchers
-end
-
-RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
- include ActiveSupport::Testing::TimeHelpers
-
- let(:klass) do
- strong_memoize_class = described_class
-
- Struct.new(:value) do
- include strong_memoize_class
-
- def self.method_added_list
- @method_added_list ||= []
- end
-
- def self.method_added(name)
- method_added_list << name
- end
-
- def method_name
- strong_memoize(:method_name) do # rubocop: disable Gitlab/StrongMemoizeAttr
- trace << value
- value
- end
- end
-
- def method_name_with_expiration
- strong_memoize_with_expiration(:method_name_with_expiration, 1) do
- trace << value
- value
- end
- end
-
- def method_name_attr
- trace << value
- value
- end
- strong_memoize_attr :method_name_attr
-
- def enabled?
- trace << value
- value
- end
- strong_memoize_attr :enabled?
-
- def method_name_with_args(*args)
- strong_memoize_with(:method_name_with_args, args) do
- trace << [value, args]
- value
- end
- end
-
- def trace
- @trace ||= []
- end
-
- protected
-
- def private_method; end
- private :private_method
- strong_memoize_attr :private_method
-
- public
-
- def protected_method; end
- protected :protected_method
- strong_memoize_attr :protected_method
-
- private
-
- def public_method; end
- public :public_method
- strong_memoize_attr :public_method
- end
- end
-
- subject(:object) { klass.new(value) }
-
- shared_examples 'caching the value' do
- let(:member_name) { described_class.normalize_key(method_name) }
-
- it 'only calls the block once' do
- value0 = object.send(method_name)
- value1 = object.send(method_name)
-
- expect(value0).to eq(value)
- expect(value1).to eq(value)
- expect(object.trace).to contain_exactly(value)
- end
-
- it 'returns and defines the instance variable for the exact value' do
- returned_value = object.send(method_name)
- memoized_value = object.instance_variable_get(:"@#{member_name}")
-
- expect(returned_value).to eql(value)
- expect(memoized_value).to eql(value)
- end
- end
-
- describe '#strong_memoize' do
- [nil, false, true, 'value', 0, [0]].each do |value|
- context "with value #{value}" do
- let(:value) { value }
- let(:method_name) { :method_name }
-
- it_behaves_like 'caching the value'
-
- it 'raises exception for invalid type as key' do
- expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/
- end
-
- it 'raises exception for invalid characters in key' do
- expect { object.strong_memoize(:enabled?) { 20 } }
- .to raise_error /is not allowed as an instance variable name/
- end
- end
- end
-
- context "memory allocation", type: :benchmark do
- let(:value) { 'aaa' }
-
- before do
- object.method_name # warmup
- end
-
- [:method_name, "method_name"].each do |argument|
- context "for #{argument.class}" do
- it 'does allocate exactly one string when fetching value' do
- expect do
- object.strong_memoize(argument) { 10 }
- end.to perform_allocation(1)
- end
-
- it 'does allocate exactly one string when storing value' do
- object.clear_memoization(:method_name) # clear to force set
-
- expect do
- object.strong_memoize(argument) { 10 }
- end.to perform_allocation(1)
- end
- end
- end
- end
- end
-
- describe '#strong_memoize_with_expiration' do
- [nil, false, true, 'value', 0, [0]].each do |value|
- context "with value #{value}" do
- let(:value) { value }
- let(:method_name) { :method_name_with_expiration }
-
- it_behaves_like 'caching the value'
-
- it 'raises exception for invalid type as key' do
- expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error /Invalid type of '10'/
- end
-
- it 'raises exception for invalid characters in key' do
- expect { object.strong_memoize_with_expiration(:enabled?, 1) { 20 } }
- .to raise_error /is not allowed as an instance variable name/
- end
- end
- end
-
- context 'value memoization test' do
- let(:value) { 'value' }
-
- it 'caches the value for specified number of seconds' do
- object.method_name_with_expiration
- object.method_name_with_expiration
-
- expect(object.trace.count).to eq(1)
-
- travel_to(Time.current + 2.seconds) do
- object.method_name_with_expiration
-
- expect(object.trace.count).to eq(2)
- end
- end
- end
- end
-
- describe '#strong_memoize_with' do
- [nil, false, true, 'value', 0, [0]].each do |value|
- context "with value #{value}" do
- let(:value) { value }
-
- it 'only calls the block once' do
- value0 = object.method_name_with_args(1)
- value1 = object.method_name_with_args(1)
- value2 = object.method_name_with_args([2, 3])
- value3 = object.method_name_with_args([2, 3])
-
- expect(value0).to eq(value)
- expect(value1).to eq(value)
- expect(value2).to eq(value)
- expect(value3).to eq(value)
-
- expect(object.trace).to contain_exactly([value, [1]], [value, [[2, 3]]])
- end
-
- it 'returns and defines the instance variable for the exact value' do
- returned_value = object.method_name_with_args(1, 2, 3)
- memoized_value = object.instance_variable_get(:@method_name_with_args)
-
- expect(returned_value).to eql(value)
- expect(memoized_value).to eql({ [[1, 2, 3]] => value })
- end
- end
- end
- end
-
- describe '#strong_memoized?' do
- shared_examples 'memoization check' do |method_name|
- context "for #{method_name}" do
- let(:value) { :anything }
-
- subject { object.strong_memoized?(method_name) }
-
- it 'returns false if the value is uncached' do
- is_expected.to be(false)
- end
-
- it 'returns true if the value is cached' do
- object.public_send(method_name)
-
- is_expected.to be(true)
- end
- end
- end
-
- it_behaves_like 'memoization check', :method_name
- it_behaves_like 'memoization check', :enabled?
- end
-
- describe '#clear_memoization' do
- shared_examples 'clearing memoization' do |method_name|
- let(:member_name) { described_class.normalize_key(method_name) }
- let(:value) { 'mepmep' }
-
- it 'removes the instance variable' do
- object.public_send(method_name)
-
- object.clear_memoization(method_name)
-
- expect(object.instance_variable_defined?(:"@#{member_name}")).to be(false)
- end
- end
-
- it_behaves_like 'clearing memoization', :method_name
- it_behaves_like 'clearing memoization', :enabled?
- end
-
- describe '.strong_memoize_attr' do
- [nil, false, true, 'value', 0, [0]].each do |value|
- context "with value '#{value}'" do
- let(:value) { value }
-
- context 'memoized after method definition' do
- let(:method_name) { :method_name_attr }
-
- it_behaves_like 'caching the value'
-
- it 'calls the existing .method_added' do
- expect(klass.method_added_list).to include(:method_name_attr)
- end
-
- it 'retains method arity' do
- expect(klass.instance_method(method_name).arity).to eq(0)
- end
- end
- end
- end
-
- describe 'method visibility' do
- it 'sets private visibility' do
- expect(klass.private_instance_methods).to include(:private_method)
- expect(klass.protected_instance_methods).not_to include(:private_method)
- expect(klass.public_instance_methods).not_to include(:private_method)
- end
-
- it 'sets protected visibility' do
- expect(klass.private_instance_methods).not_to include(:protected_method)
- expect(klass.protected_instance_methods).to include(:protected_method)
- expect(klass.public_instance_methods).not_to include(:protected_method)
- end
-
- it 'sets public visibility' do
- expect(klass.private_instance_methods).not_to include(:public_method)
- expect(klass.protected_instance_methods).not_to include(:public_method)
- expect(klass.public_instance_methods).to include(:public_method)
- end
- end
-
- context "when method doesn't exist" do
- let(:klass) do
- strong_memoize_class = described_class
-
- Struct.new(:value) do
- include strong_memoize_class
- end
- end
-
- subject { klass.strong_memoize_attr(:nonexistent_method) }
-
- it 'fails when strong-memoizing a nonexistent method' do
- expect { subject }.to raise_error(NameError, %r{undefined method `nonexistent_method' for class})
- end
- end
-
- context 'when memoized method has parameters' do
- it 'raises an error' do
- expected_message = /Using `strong_memoize_attr` on methods with parameters is not supported/
-
- expect do
- strong_memoize_class = described_class
-
- Class.new do
- include strong_memoize_class
-
- def method_with_parameters(params); end
- strong_memoize_attr :method_with_parameters
- end
- end.to raise_error(RuntimeError, expected_message)
- end
- end
- end
-
- describe '.normalize_key' do
- using RSpec::Parameterized::TableSyntax
-
- subject { described_class.normalize_key(input) }
-
- where(:input, :output, :valid) do
- :key | :key | true
- "key" | "key" | true
- :key? | "key?" | true
- "key?" | "key?" | true
- :key! | "key!" | true
- "key!" | "key!" | true
- # invalid cases caught elsewhere
- :"ke?y" | :"ke?y" | false
- "ke?y" | "ke?y" | false
- :"ke!y" | :"ke!y" | false
- "ke!y" | "ke!y" | false
- end
-
- with_them do
- let(:ivar) { "@#{output}" }
-
- it { is_expected.to eq(output) }
-
- if params[:valid]
- it 'is a valid ivar name' do
- expect { instance_variable_defined?(ivar) }.not_to raise_error
- end
- else
- it 'raises a NameError error' do
- expect { instance_variable_defined?(ivar) }
- .to raise_error(NameError, /not allowed as an instance/)
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
deleted file mode 100644
index 7b9504366ec..00000000000
--- a/spec/lib/gitlab/utils_spec.rb
+++ /dev/null
@@ -1,477 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Utils do
- using RSpec::Parameterized::TableSyntax
-
- delegate :to_boolean, :boolean_to_yes_no, :slugify, :which,
- :ensure_array_from_string, :bytes_to_megabytes,
- :append_path, :remove_leading_slashes, :allowlisted?,
- :decode_path, :ms_to_round_sec, to: :described_class
-
- describe '.allowlisted?' do
- let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd'] }
-
- it 'returns true if path is allowed' do
- expect(allowlisted?('/foo/bar', allowed_paths)).to be(true)
- end
-
- it 'returns false if path is not allowed' do
- expect(allowlisted?('/test/test', allowed_paths)).to be(false)
- end
- end
-
- describe '.decode_path' do
- it 'returns path unencoded for singled-encoded paths' do
- expect(decode_path('%2Fhome%2Fbar%3Fasd%3Dqwe')).to eq('/home/bar?asd=qwe')
- end
-
- it 'returns path when it is unencoded' do
- expect(decode_path('/home/bar?asd=qwe')).to eq('/home/bar?asd=qwe')
- end
-
- [
- '..%252F..%252F..%252Fetc%252Fpasswd',
- '%25252Fresult%25252Fchosennickname%25253D%252522jj%252522'
- ].each do |multiple_encoded_path|
- it 'raises an exception when the path is multiple-encoded' do
- expect { decode_path(multiple_encoded_path) }.to raise_error(/path #{multiple_encoded_path} is not allowed/)
- end
- end
- end
-
- describe '.slugify' do
- {
- 'TEST' => 'test',
- 'project_with_underscores' => 'project-with-underscores',
- 'namespace/project' => 'namespace-project',
- 'a' * 70 => 'a' * 63,
- 'test_trailing_' => 'test-trailing'
- }.each do |original, expected|
- it "slugifies #{original} to #{expected}" do
- expect(slugify(original)).to eq(expected)
- end
- end
- end
-
- describe '.ms_to_round_sec' do
- where(:original, :expected) do
- 1999.8999 | 1.9999
- 12384 | 12.384
- 333 | 0.333
- 1333.33333333 | 1.333333
- end
-
- with_them do
- it "returns rounded seconds" do
- expect(ms_to_round_sec(original)).to eq(expected)
- end
- end
- end
-
- describe '.nlbr' do
- it 'replaces new lines with <br>' do
- expect(described_class.nlbr("<b>hello</b>\n<i>world</i>")).to eq("hello<br>world")
- end
- end
-
- describe '.remove_line_breaks' do
- where(:original, :expected) do
- "foo\nbar\nbaz" | "foobarbaz"
- "foo\r\nbar\r\nbaz" | "foobarbaz"
- "foobar" | "foobar"
- end
-
- with_them do
- it "replace line breaks with an empty string" do
- expect(described_class.remove_line_breaks(original)).to eq(expected)
- end
- end
- end
-
- describe '.to_boolean' do
- it 'accepts booleans' do
- expect(to_boolean(true)).to be(true)
- expect(to_boolean(false)).to be(false)
- end
-
- it 'converts a valid value to a boolean' do
- expect(to_boolean(true)).to be(true)
- expect(to_boolean('true')).to be(true)
- expect(to_boolean('YeS')).to be(true)
- expect(to_boolean('t')).to be(true)
- expect(to_boolean('1')).to be(true)
- expect(to_boolean(1)).to be(true)
- expect(to_boolean('ON')).to be(true)
-
- expect(to_boolean('FaLse')).to be(false)
- expect(to_boolean('F')).to be(false)
- expect(to_boolean('NO')).to be(false)
- expect(to_boolean('n')).to be(false)
- expect(to_boolean('0')).to be(false)
- expect(to_boolean(0)).to be(false)
- expect(to_boolean('oFF')).to be(false)
- end
-
- it 'converts an invalid value to nil' do
- expect(to_boolean('fals')).to be_nil
- expect(to_boolean('yeah')).to be_nil
- expect(to_boolean('')).to be_nil
- expect(to_boolean(nil)).to be_nil
- end
-
- it 'accepts a default value, and does not return it when a valid value is given' do
- expect(to_boolean(true, default: false)).to be(true)
- expect(to_boolean('true', default: false)).to be(true)
- expect(to_boolean('YeS', default: false)).to be(true)
- expect(to_boolean('t', default: false)).to be(true)
- expect(to_boolean('1', default: 'any value')).to be(true)
- expect(to_boolean('ON', default: 42)).to be(true)
-
- expect(to_boolean('FaLse', default: true)).to be(false)
- expect(to_boolean('F', default: true)).to be(false)
- expect(to_boolean('NO', default: true)).to be(false)
- expect(to_boolean('n', default: true)).to be(false)
- expect(to_boolean('0', default: 'any value')).to be(false)
- expect(to_boolean('oFF', default: 42)).to be(false)
- end
-
- it 'accepts a default value, and returns it when an invalid value is given' do
- expect(to_boolean('fals', default: true)).to eq(true)
- expect(to_boolean('yeah', default: false)).to eq(false)
- expect(to_boolean('', default: 'any value')).to eq('any value')
- expect(to_boolean(nil, default: 42)).to eq(42)
- end
- end
-
- describe '.boolean_to_yes_no' do
- it 'converts booleans to Yes or No' do
- expect(boolean_to_yes_no(true)).to eq('Yes')
- expect(boolean_to_yes_no(false)).to eq('No')
- end
- end
-
- describe '.which' do
- before do
- stub_env('PATH', '/sbin:/usr/bin:/home/joe/bin')
- end
-
- it 'finds the full path to an executable binary in order of appearance' do
- expect(File).to receive(:executable?).with('/sbin/tool').ordered.and_return(false)
- expect(File).to receive(:executable?).with('/usr/bin/tool').ordered.and_return(true)
- expect(File).not_to receive(:executable?).with('/home/joe/bin/tool')
-
- expect(which('tool')).to eq('/usr/bin/tool')
- end
- end
-
- describe '.ensure_array_from_string' do
- it 'returns the same array if given one' do
- arr = ['a', 4, true, { test: 1 }]
-
- expect(ensure_array_from_string(arr)).to eq(arr)
- end
-
- it 'turns comma-separated strings into arrays' do
- str = 'seven, eight, 9, 10'
-
- expect(ensure_array_from_string(str)).to eq(%w[seven eight 9 10])
- end
- end
-
- describe '.bytes_to_megabytes' do
- it 'converts bytes to megabytes' do
- bytes = 1.megabyte
-
- expect(bytes_to_megabytes(bytes)).to eq(1)
- end
- end
-
- describe '.append_path' do
- where(:host, :path, :result) do
- 'http://test/' | '/foo/bar' | 'http://test/foo/bar'
- 'http://test/' | '//foo/bar' | 'http://test/foo/bar'
- 'http://test//' | '/foo/bar' | 'http://test/foo/bar'
- 'http://test' | 'foo/bar' | 'http://test/foo/bar'
- 'http://test//' | '' | 'http://test/'
- 'http://test//' | nil | 'http://test/'
- '' | '/foo/bar' | '/foo/bar'
- nil | '/foo/bar' | '/foo/bar'
- end
-
- with_them do
- it 'makes sure there is only one slash as path separator' do
- expect(append_path(host, path)).to eq(result)
- end
- end
- end
-
- describe '.remove_leading_slashes' do
- where(:str, :result) do
- '/foo/bar' | 'foo/bar'
- '//foo/bar' | 'foo/bar'
- '/foo/bar/' | 'foo/bar/'
- 'foo/bar' | 'foo/bar'
- '' | ''
- nil | ''
- end
-
- with_them do
- it 'removes leading slashes' do
- expect(remove_leading_slashes(str)).to eq(result)
- end
- end
- end
-
- describe '.ensure_utf8_size' do
- context 'string is has less bytes than expected' do
- it 'backfills string with null characters' do
- transformed = described_class.ensure_utf8_size('a' * 10, bytes: 32)
-
- expect(transformed.bytesize).to eq 32
- expect(transformed).to eq(('a' * 10) + ('0' * 22))
- end
- end
-
- context 'string size is exactly the one that is expected' do
- it 'returns original value' do
- transformed = described_class.ensure_utf8_size('a' * 32, bytes: 32)
-
- expect(transformed).to eq 'a' * 32
- expect(transformed.bytesize).to eq 32
- end
- end
-
- context 'when string contains a few multi-byte UTF characters' do
- it 'backfills string with null characters' do
- transformed = described_class.ensure_utf8_size('❤' * 6, bytes: 32)
-
- expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14)
- expect(transformed.bytesize).to eq 32
- end
- end
-
- context 'when string has multiple multi-byte UTF chars exceeding 32 bytes' do
- it 'truncates string to 32 characters and backfills it if needed' do
- transformed = described_class.ensure_utf8_size('❤' * 18, bytes: 32)
-
- expect(transformed).to eq(('❤' * 10) + ('0' * 2))
- expect(transformed.bytesize).to eq 32
- end
- end
- end
-
- describe '.deep_indifferent_access' do
- let(:hash) do
- { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] }
- end
-
- subject { described_class.deep_indifferent_access(hash) }
-
- it 'allows to access hash keys with symbols' do
- expect(subject[:variables]).to be_a(Array)
- end
-
- it 'allows to access array keys with symbols' do
- expect(subject[:variables].first[:key]).to eq('VAR1')
- end
- end
-
- describe '.deep_symbolized_access' do
- let(:hash) do
- { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] }
- end
-
- subject { described_class.deep_symbolized_access(hash) }
-
- it 'allows to access hash keys with symbols' do
- expect(subject[:variables]).to be_a(Array)
- end
-
- it 'allows to access array keys with symbols' do
- expect(subject[:variables].first[:key]).to eq('VAR1')
- end
- end
-
- describe '.try_megabytes_to_bytes' do
- context 'when the size can be converted to megabytes' do
- it 'returns the size in megabytes' do
- size = described_class.try_megabytes_to_bytes(1)
-
- expect(size).to eq(1.megabytes)
- end
- end
-
- context 'when the size can not be converted to megabytes' do
- it 'returns the input size' do
- size = described_class.try_megabytes_to_bytes('foo')
-
- expect(size).to eq('foo')
- end
- end
- end
-
- describe '.string_to_ip_object' do
- it 'returns nil when string is nil' do
- expect(described_class.string_to_ip_object(nil)).to eq(nil)
- end
-
- it 'returns nil when string is invalid IP' do
- expect(described_class.string_to_ip_object('invalid ip')).to eq(nil)
- expect(described_class.string_to_ip_object('')).to eq(nil)
- end
-
- it 'returns IP object when string is valid IP' do
- expect(described_class.string_to_ip_object('192.168.1.1')).to eq(IPAddr.new('192.168.1.1'))
- expect(described_class.string_to_ip_object('::ffff:a9fe:a864')).to eq(IPAddr.new('::ffff:a9fe:a864'))
- expect(described_class.string_to_ip_object('[::ffff:a9fe:a864]')).to eq(IPAddr.new('::ffff:a9fe:a864'))
- expect(described_class.string_to_ip_object('127.0.0.0/28')).to eq(IPAddr.new('127.0.0.0/28'))
- expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124'))
- end
- end
-
- describe ".safe_downcase!" do
- where(:str, :result) do
- "test" | "test"
- "Test" | "test"
- "test" | "test"
- "Test" | "test"
- end
-
- with_them do
- it "downcases the string" do
- expect(described_class.safe_downcase!(str)).to eq(result)
- end
- end
- end
-
- describe '.parse_url' do
- it 'returns Addressable::URI object' do
- expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI)
- end
-
- it 'returns nil when URI cannot be parsed' do
- expect(described_class.parse_url('://gitlab.com')).to be nil
- end
-
- it 'returns nil with invalid parameter' do
- expect(described_class.parse_url(1)).to be nil
- end
- end
-
- describe '.add_url_parameters' do
- subject { described_class.add_url_parameters(url, params) }
-
- where(:url, :params, :expected_url) do
- nil | nil | ''
- nil | { b: 3, a: 2 } | '?a=2&b=3'
- 'https://gitlab.com' | nil | 'https://gitlab.com'
- 'https://gitlab.com' | { b: 3, a: 2 } | 'https://gitlab.com?a=2&b=3'
- 'https://gitlab.com?a=1#foo' | { b: 3, 'a': 2 } | 'https://gitlab.com?a=2&b=3#foo'
- 'https://gitlab.com?a=1#foo' | [[:b, 3], [:a, 2]] | 'https://gitlab.com?a=2&b=3#foo'
- end
-
- with_them do
- it { is_expected.to eq(expected_url) }
- end
- end
-
- describe '.removes_sensitive_data_from_url' do
- it 'returns string object' do
- expect(described_class.removes_sensitive_data_from_url('http://gitlab.com')).to be_instance_of(String)
- end
-
- it 'returns nil when URI cannot be parsed' do
- expect(described_class.removes_sensitive_data_from_url('://gitlab.com')).to be nil
- end
-
- it 'returns nil with invalid parameter' do
- expect(described_class.removes_sensitive_data_from_url(1)).to be nil
- end
-
- it 'returns string with filtered access_token param' do
- expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token')).to eq('http://gitlab.com/auth.html#access_token=filtered')
- end
-
- it 'returns string with filtered access_token param but other params preserved' do
- expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token&token_type=Bearer&state=test'))
- .to include('&token_type=Bearer', '&state=test')
- end
- end
-
- describe 'multiple_key_invert' do
- it 'invert keys with array values' do
- hash = {
- dast: [:vulnerabilities_count, :scanned_resources_count],
- sast: [:vulnerabilities_count]
- }
- expect(described_class.multiple_key_invert(hash)).to eq({
- vulnerabilities_count: [:dast, :sast],
- scanned_resources_count: [:dast]
- })
- end
- end
-
- describe '.stable_sort_by' do
- subject(:sorted_list) { described_class.stable_sort_by(list) { |obj| obj[:priority] } }
-
- context 'when items have the same priority' do
- let(:list) do
- [
- { name: 'obj 1', priority: 1 },
- { name: 'obj 2', priority: 1 },
- { name: 'obj 3', priority: 1 }
- ]
- end
-
- it 'does not change order in cases of ties' do
- expect(sorted_list).to eq(list)
- end
- end
-
- context 'when items have different priorities' do
- let(:list) do
- [
- { name: 'obj 1', priority: 2 },
- { name: 'obj 2', priority: 1 },
- { name: 'obj 3', priority: 3 }
- ]
- end
-
- it 'sorts items like the regular sort_by' do
- expect(sorted_list).to eq(
- [
- { name: 'obj 2', priority: 1 },
- { name: 'obj 1', priority: 2 },
- { name: 'obj 3', priority: 3 }
- ])
- end
- end
- end
-
- describe '.valid_brackets?' do
- where(:input, :allow_nested, :valid) do
- 'no brackets' | true | true
- 'no brackets' | false | true
- 'user[avatar]' | true | true
- 'user[avatar]' | false | true
- 'user[avatar][friends]' | true | true
- 'user[avatar][friends]' | false | true
- 'user[avatar[image[url]]]' | true | true
- 'user[avatar[image[url]]]' | false | false
- 'user[avatar[]friends]' | true | true
- 'user[avatar[]friends]' | false | false
- 'user[avatar]]' | true | false
- 'user[avatar]]' | false | false
- 'user][avatar]]' | true | false
- 'user][avatar]]' | false | false
- 'user[avatar' | true | false
- 'user[avatar' | false | false
- end
-
- with_them do
- it { expect(described_class.valid_brackets?(input, allow_nested: allow_nested)).to eq(valid) }
- end
- end
-end
diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb
deleted file mode 100644
index 99c7a762392..00000000000
--- a/spec/lib/gitlab/version_info_spec.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::VersionInfo do
- before do
- @unknown = described_class.new
- @v0_0_1 = described_class.new(0, 0, 1)
- @v0_1_0 = described_class.new(0, 1, 0)
- @v1_0_0 = described_class.new(1, 0, 0)
- @v1_0_1 = described_class.new(1, 0, 1)
- @v1_0_1_b1 = described_class.new(1, 0, 1, '-b1')
- @v1_0_1_rc1 = described_class.new(1, 0, 1, '-rc1')
- @v1_0_1_rc2 = described_class.new(1, 0, 1, '-rc2')
- @v1_1_0 = described_class.new(1, 1, 0)
- @v1_1_0_beta1 = described_class.new(1, 1, 0, '-beta1')
- @v2_0_0 = described_class.new(2, 0, 0)
- @v13_10_1_1574_89 = described_class.parse("v13.10.1~beta.1574.gf6ea9389", parse_suffix: true)
- @v13_10_1_1575_89 = described_class.parse("v13.10.1~beta.1575.gf6ea9389", parse_suffix: true)
- @v13_10_1_1575_90 = described_class.parse("v13.10.1~beta.1575.gf6ea9390", parse_suffix: true)
- end
-
- describe '>' do
- it { expect(@v2_0_0).to be > @v1_1_0 }
- it { expect(@v1_1_0).to be > @v1_0_1 }
- it { expect(@v1_0_1_b1).to be > @v1_0_0 }
- it { expect(@v1_0_1_rc1).to be > @v1_0_0 }
- it { expect(@v1_0_1_rc1).to be > @v1_0_1_b1 }
- it { expect(@v1_0_1_rc2).to be > @v1_0_1_rc1 }
- it { expect(@v1_0_1).to be > @v1_0_1_rc1 }
- it { expect(@v1_0_1).to be > @v1_0_1_rc2 }
- it { expect(@v1_0_1).to be > @v1_0_0 }
- it { expect(@v1_0_0).to be > @v0_1_0 }
- it { expect(@v1_1_0_beta1).to be > @v1_0_1_rc2 }
- it { expect(@v1_1_0).to be > @v1_1_0_beta1 }
- it { expect(@v0_1_0).to be > @v0_0_1 }
- end
-
- describe '>=' do
- it { expect(@v2_0_0).to be >= described_class.new(2, 0, 0) }
- it { expect(@v2_0_0).to be >= @v1_1_0 }
- it { expect(@v1_0_1_rc2).to be >= @v1_0_1_rc1 }
- end
-
- describe '<' do
- it { expect(@v0_0_1).to be < @v0_1_0 }
- it { expect(@v0_1_0).to be < @v1_0_0 }
- it { expect(@v1_0_0).to be < @v1_0_1 }
- it { expect(@v1_0_1).to be < @v1_1_0 }
- it { expect(@v1_0_0).to be < @v1_0_1_rc2 }
- it { expect(@v1_0_1_rc1).to be < @v1_0_1 }
- it { expect(@v1_0_1_rc1).to be < @v1_0_1_rc2 }
- it { expect(@v1_0_1_rc2).to be < @v1_0_1 }
- it { expect(@v1_1_0).to be < @v2_0_0 }
- it { expect(@v13_10_1_1574_89).to be < @v13_10_1_1575_89 }
- it { expect(@v13_10_1_1575_89).to be < @v13_10_1_1575_90 }
- end
-
- describe '<=' do
- it { expect(@v0_0_1).to be <= described_class.new(0, 0, 1) }
- it { expect(@v0_0_1).to be <= @v0_1_0 }
- it { expect(@v1_0_1_b1).to be <= @v1_0_1_rc1 }
- it { expect(@v1_0_1_rc1).to be <= @v1_0_1_rc2 }
- it { expect(@v1_1_0_beta1).to be <= @v1_1_0 }
- end
-
- describe '==' do
- it { expect(@v0_0_1).to eq(described_class.new(0, 0, 1)) }
- it { expect(@v0_1_0).to eq(described_class.new(0, 1, 0)) }
- it { expect(@v1_0_0).to eq(described_class.new(1, 0, 0)) }
- it { expect(@v1_0_1_rc1).to eq(described_class.new(1, 0, 1, '-rc1')) }
- end
-
- describe '!=' do
- it { expect(@v0_0_1).not_to eq(@v0_1_0) }
- it { expect(@v1_0_1_rc1).not_to eq(@v1_0_1_rc2) }
- end
-
- describe '.unknown' do
- it { expect(@unknown).not_to be @v0_0_1 }
- it { expect(@unknown).not_to be described_class.new }
- it { expect { @unknown > @v0_0_1 }.to raise_error(ArgumentError) }
- it { expect { @unknown < @v0_0_1 }.to raise_error(ArgumentError) }
- end
-
- describe '.parse' do
- it { expect(described_class.parse(described_class.new(1, 0, 0))).to eq(@v1_0_0) }
- it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) }
- it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) }
- it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) }
- it { expect(described_class.parse("1.0.0-rc1")).to eq(@v1_0_0) }
- it { expect(described_class.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) }
- it { expect(described_class.parse("git 1.0.0b1")).to eq(@v1_0_0) }
- it { expect(described_class.parse("git 1.0b1")).not_to be_valid }
- it { expect(described_class.parse("1.1.#{'1' * described_class::MAX_VERSION_LENGTH}")).not_to be_valid }
- it { expect(described_class.parse(nil)).not_to be_valid }
-
- context 'with parse_suffix: true' do
- let(:versions) do
- <<-VERSIONS.lines
- 0.0.1
- 0.1.0
- 1.0.0
- 1.0.1-b1
- 1.0.1-rc1
- 1.0.1-rc2
- 1.0.1
- 1.1.0-beta1
- 1.1.0
- 2.0.0
- v13.10.0-pre
- v13.10.0-rc1
- v13.10.0-rc2
- v13.10.0
- v13.10.1~beta.1574.gf6ea9389
- v13.10.1~beta.1575.gf6ea9389
- v13.10.1-rc1
- v13.10.1-rc2
- v13.10.1
- VERSIONS
- end
-
- let(:parsed_versions) do
- versions.map(&:strip).map { |version| described_class.parse(version, parse_suffix: true) }
- end
-
- it 'versions are returned in a correct order' do
- expect(parsed_versions.shuffle.sort).to eq(parsed_versions)
- end
- end
- end
-
- describe '.to_s' do
- it { expect(@v1_0_0.to_s).to eq("1.0.0") }
- it { expect(@v1_0_1_rc1.to_s).to eq("1.0.1-rc1") }
- it { expect(@unknown.to_s).to eq("Unknown") }
- end
-
- describe '.to_json' do
- let(:correct_version) do
- "{\"major\":1,\"minor\":0,\"patch\":1}"
- end
-
- let(:unknown_version) do
- "{\"major\":0,\"minor\":0,\"patch\":0}"
- end
-
- it { expect(@v1_0_1.to_json).to eq(correct_version) }
- it { expect(@v1_0_1_rc2.to_json).to eq(correct_version) }
- it { expect(@unknown.to_json).to eq(unknown_version) }
- end
-
- describe '.hash' do
- it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) }
- it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) }
- it { expect(described_class.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) }
- it { expect(described_class.parse("1.0.1-rc1", parse_suffix: true).hash).to eq(@v1_0_1_rc1.hash) }
- end
-
- describe '.eql?' do
- it { expect(described_class.parse("1.0.0").eql?(@v1_0_0)).to be_truthy }
- it { expect(described_class.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy }
- it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc1)).to be_truthy }
- it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc2)).to be_falsey }
- it { expect(@v1_0_1_rc1.eql?(@v1_0_1)).to be_falsey }
- it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey }
- it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey }
- it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy }
- it { expect([@v1_0_0, @v1_1_0, @v1_0_0, @v1_0_1_rc1, @v1_0_1_rc1].uniq).to eq [@v1_0_0, @v1_1_0, @v1_0_1_rc1] }
- end
-
- describe '.same_minor_version?' do
- it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey }
- it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy }
- it { expect(@v1_0_1_rc1.same_minor_version?(@v1_0_0)).to be_truthy }
- it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy }
- it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey }
- it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey }
- end
-
- describe '.without_patch' do
- it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) }
- it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) }
- it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) }
- it { expect(@v1_0_1_rc1.without_patch).to eq(@v1_0_0) }
- end
-
- describe 'MAX_VERSION_LENGTH' do
- subject { described_class::MAX_VERSION_LENGTH }
-
- it { is_expected.to eq(128) }
- end
-end
diff --git a/spec/lib/gitlab/webpack/file_loader_spec.rb b/spec/lib/gitlab/webpack/file_loader_spec.rb
index c2e9cd8124d..16bf380fd13 100644
--- a/spec/lib/gitlab/webpack/file_loader_spec.rb
+++ b/spec/lib/gitlab/webpack/file_loader_spec.rb
@@ -32,15 +32,15 @@ RSpec.describe Gitlab::Webpack::FileLoader do
end
it "returns content when responds successfully" do
- expect(Gitlab::Webpack::FileLoader.load(file_path)).to eq(file_contents)
+ expect(described_class.load(file_path)).to eq(file_contents)
end
it "raises error when 404" do
- expect { Gitlab::Webpack::FileLoader.load("not_found") }.to raise_error("HTTP error 404")
+ expect { described_class.load("not_found") }.to raise_error("HTTP error 404")
end
it "raises error when errors out" do
- expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerLoadError)
+ expect { described_class.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerLoadError)
end
end
@@ -53,7 +53,7 @@ RSpec.describe Gitlab::Webpack::FileLoader do
end
it "raises error if catches SSLError" do
- expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerSSLError)
+ expect { described_class.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerSSLError)
end
end
@@ -66,11 +66,11 @@ RSpec.describe Gitlab::Webpack::FileLoader do
describe ".load" do
it "returns file content from file path" do
- expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
+ expect(described_class.load(file_path)).to be(file_contents)
end
it "throws error if file cannot be read" do
- expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::StaticLoadError)
+ expect { described_class.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::StaticLoadError)
end
end
end
diff --git a/spec/lib/gitlab/webpack/manifest_spec.rb b/spec/lib/gitlab/webpack/manifest_spec.rb
index 24a36258379..cbf3e4d2951 100644
--- a/spec/lib/gitlab/webpack/manifest_spec.rb
+++ b/spec/lib/gitlab/webpack/manifest_spec.rb
@@ -17,24 +17,24 @@ RSpec.describe Gitlab::Webpack::Manifest do
end
around do |example|
- Gitlab::Webpack::Manifest.clear_manifest!
+ described_class.clear_manifest!
example.run
- Gitlab::Webpack::Manifest.clear_manifest!
+ described_class.clear_manifest!
end
shared_examples_for "a valid manifest" do
it "returns single entry asset paths from the manifest" do
- expect(Gitlab::Webpack::Manifest.asset_paths("entry2")).to eq(["/public_path/entry2.js"])
+ expect(described_class.asset_paths("entry2")).to eq(["/public_path/entry2.js"])
end
it "returns multiple entry asset paths from the manifest" do
- expect(Gitlab::Webpack::Manifest.asset_paths("entry1")).to eq(["/public_path/entry1.js", "/public_path/entry1-a.js"])
+ expect(described_class.asset_paths("entry1")).to eq(["/public_path/entry1.js", "/public_path/entry1-a.js"])
end
it "errors on a missing entry point" do
- expect { Gitlab::Webpack::Manifest.asset_paths("herp") }.to raise_error(Gitlab::Webpack::Manifest::AssetMissingError)
+ expect { described_class.asset_paths("herp") }.to raise_error(Gitlab::Webpack::Manifest::AssetMissingError)
end
end
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::Webpack::Manifest do
allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json')
stub_request(:get, "http://hostname:2000/public_path/broken.json").to_raise(SocketError)
- expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
+ expect { described_class.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
end
describe "webpack errors" do
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::Webpack::Manifest do
]).to_json
stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200)
- expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::WebpackError)
+ expect { described_class.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::WebpackError)
end
end
@@ -82,14 +82,14 @@ RSpec.describe Gitlab::Webpack::Manifest do
error_manifest = Gitlab::Json.parse(manifest).merge("errors" => ["something went wrong"]).to_json
stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200)
- expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.not_to raise_error
+ expect { described_class.asset_paths("entry1") }.not_to raise_error
end
end
it "does not error if errors is present but empty" do
error_manifest = Gitlab::Json.parse(manifest).merge("errors" => []).to_json
stub_request(:get, "http://hostname:2000/public_path/my_manifest.json").to_return(body: error_manifest, status: 200)
- expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.not_to raise_error
+ expect { described_class.asset_paths("entry1") }.not_to raise_error
end
end
end
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Webpack::Manifest do
it "errors if we can't find the manifest" do
allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json')
stub_file_read(::Rails.root.join("manifest_output/broken.json"), error: Errno::ENOENT)
- expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
+ expect { described_class.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError)
end
end
end
diff --git a/spec/lib/gitlab/x509/commit_spec.rb b/spec/lib/gitlab/x509/commit_spec.rb
index c7d56e49fab..412fa6e5a7f 100644
--- a/spec/lib/gitlab/x509/commit_spec.rb
+++ b/spec/lib/gitlab/x509/commit_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::X509::Commit do
let(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
let(:project) { create(:project, :repository, path: X509Helpers::User1.path, creator: user) }
let(:commit) { project.commit_by(oid: commit_sha ) }
- let(:signature) { Gitlab::X509::Commit.new(commit).signature }
+ let(:signature) { described_class.new(commit).signature }
let(:store) { OpenSSL::X509::Store.new }
let(:certificate) { OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert) }
diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb
index eb8c0bd0aff..d119a4e2b9d 100644
--- a/spec/lib/gitlab/x509/signature_spec.rb
+++ b/spec/lib/gitlab/x509/signature_spec.rb
@@ -197,9 +197,9 @@ RSpec.describe Gitlab::X509::Signature do
context 'certificate_crl' do
describe 'valid crlDistributionPoints' do
before do
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension)
.with('crlDistributionPoints')
.and_return("\nFull Name:\n URI:http://ch.siemens.com/pki?ZZZZZZA2.crl\n URI:ldap://cl.siemens.net/CN=ZZZZZZA2,L=PKI?certificateRevocationList\n URI:ldap://cl.siemens.com/CN=ZZZZZZA2,o=Trustcenter?certificateRevocationList\n")
end
@@ -218,9 +218,9 @@ RSpec.describe Gitlab::X509::Signature do
describe 'valid crlDistributionPoints providing multiple http URIs' do
before do
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension)
.with('crlDistributionPoints')
.and_return("\nFull Name:\n URI:http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n\nFull Name:\n URI:http://cdp2.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n")
end
@@ -241,9 +241,9 @@ RSpec.describe Gitlab::X509::Signature do
context 'email' do
describe 'subjectAltName with email, othername' do
before do
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension)
.with('subjectAltName')
.and_return("email:gitlab@example.com, othername:<unsupported>")
end
@@ -262,9 +262,9 @@ RSpec.describe Gitlab::X509::Signature do
describe 'subjectAltName with othername, email' do
before do
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension).and_call_original
- allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension)
+ allow_any_instance_of(described_class).to receive(:get_certificate_extension)
.with('subjectAltName')
.and_return("othername:<unsupported>, email:gitlab@example.com")
end
diff --git a/spec/lib/gitlab_settings/options_spec.rb b/spec/lib/gitlab_settings/options_spec.rb
index 23cb2180edd..abb895032c9 100644
--- a/spec/lib/gitlab_settings/options_spec.rb
+++ b/spec/lib/gitlab_settings/options_spec.rb
@@ -7,6 +7,33 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
subject(:options) { described_class.build(config) }
+ shared_examples 'do not mutate' do |method|
+ context 'when in production env' do
+ it 'returns the unchanged internal hash' do
+ stub_rails_env('production')
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_and_raise_for_dev_exception)
+ .with(RuntimeError.new("Warning: Do not mutate GitlabSettings::Options objects: `#{method}`"), method: method)
+ .and_call_original
+
+ expect(options.send(method)).to be_truthy
+ end
+ end
+
+ context 'when not in production env' do
+ it 'raises an exception to avoid changing the internal keys' do
+ exception = "Warning: Do not mutate GitlabSettings::Options objects: `#{method}`"
+
+ stub_rails_env('development')
+ expect { options.send(method) }.to raise_error(exception)
+
+ stub_rails_env('test')
+ expect { options.send(method) }.to raise_error(exception)
+ end
+ end
+ end
+
describe '.build' do
context 'when argument is a hash' do
it 'creates a new GitlabSettings::Options instance' do
@@ -19,6 +46,16 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
end
end
+ describe '#default' do
+ it 'returns the option value' do
+ expect(options.default).to be_nil
+
+ options['default'] = 'The default value'
+
+ expect(options.default).to eq('The default value')
+ end
+ end
+
describe '#[]' do
it 'accesses the configuration key as string' do
expect(options['foo']).to be_a described_class
@@ -96,7 +133,7 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
end
describe '#merge' do
- it 'merges a hash to the existing options' do
+ it 'returns a new object with the options merged' do
expect(options.merge(more: 'configs').to_hash).to eq(
'foo' => { 'bar' => 'baz' },
'more' => 'configs'
@@ -104,14 +141,33 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
end
context 'when the merge hash replaces existing configs' do
- it 'merges a hash to the existing options' do
+ it 'returns a new object with the duplicated options replaced' do
expect(options.merge(foo: 'configs').to_hash).to eq('foo' => 'configs')
end
end
end
+ describe '#merge!' do
+ it 'merges in place with the existing options' do
+ options.merge!(more: 'configs') # rubocop: disable Performance/RedundantMerge
+
+ expect(options.to_hash).to eq(
+ 'foo' => { 'bar' => 'baz' },
+ 'more' => 'configs'
+ )
+ end
+
+ context 'when the merge hash replaces existing configs' do
+ it 'merges in place with the duplicated options replaced' do
+ options.merge!(foo: 'configs') # rubocop: disable Performance/RedundantMerge
+
+ expect(options.to_hash).to eq('foo' => 'configs')
+ end
+ end
+ end
+
describe '#deep_merge' do
- it 'merges a hash to the existing options' do
+ it 'returns a new object with the options merged' do
expect(options.deep_merge(foo: { more: 'configs' }).to_hash).to eq('foo' => {
'bar' => 'baz',
'more' => 'configs'
@@ -119,7 +175,24 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
end
context 'when the merge hash replaces existing configs' do
- it 'merges a hash to the existing options' do
+ it 'returns a new object with the duplicated options replaced' do
+ expect(options.deep_merge(foo: { bar: 'configs' }).to_hash).to eq('foo' => {
+ 'bar' => 'configs'
+ })
+ end
+ end
+ end
+
+ describe '#deep_merge!' do
+ it 'merges in place with the existing options' do
+ expect(options.deep_merge(foo: { more: 'configs' }).to_hash).to eq('foo' => {
+ 'bar' => 'baz',
+ 'more' => 'configs'
+ })
+ end
+
+ context 'when the merge hash replaces existing configs' do
+ it 'merges in place with the duplicated options replaced' do
expect(options.deep_merge(foo: { bar: 'configs' }).to_hash).to eq('foo' => {
'bar' => 'configs'
})
@@ -135,6 +208,14 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
end
end
+ describe '#symbolize_keys!' do
+ it_behaves_like 'do not mutate', :symbolize_keys!
+ end
+
+ describe '#stringify_keys!' do
+ it_behaves_like 'do not mutate', :stringify_keys!
+ end
+
describe '#method_missing' do
context 'when method is an option' do
it 'delegates methods to options keys' do
@@ -149,10 +230,31 @@ RSpec.describe GitlabSettings::Options, :aggregate_failures, feature_category: :
end
context 'when method is not an option' do
- it 'delegates the method to the internal options hash' do
- expect { options.foo.delete('bar') }
- .to change { options.to_hash }
- .to({ 'foo' => {} })
+ context 'when in production env' do
+ it 'delegates the method to the internal options hash' do
+ stub_rails_env('production')
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_and_raise_for_dev_exception)
+ .with(RuntimeError.new('Calling a hash method on GitlabSettings::Options: `delete`'), method: :delete)
+ .and_call_original
+
+ expect { options.foo.delete('bar') }
+ .to change { options.to_hash }
+ .to({ 'foo' => {} })
+ end
+ end
+
+ context 'when not in production env' do
+ it 'delegates the method to the internal options hash' do
+ exception = 'Calling a hash method on GitlabSettings::Options: `delete`'
+
+ stub_rails_env('development')
+ expect { options.foo.delete('bar') }.to raise_error(exception)
+
+ stub_rails_env('test')
+ expect { options.foo.delete('bar') }.to raise_error(exception)
+ end
end
end
diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb
new file mode 100644
index 00000000000..2b88521fe14
--- /dev/null
+++ b/spec/lib/result_spec.rb
@@ -0,0 +1,328 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+# NOTE:
+# This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type.
+# These examples can be executed as-is in a Rails console to see the results.
+#
+# To support this, we have intentionally used some `rubocop:disable` comments to allow for more
+# explicit and readable examples.
+# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
+RSpec.describe Result, feature_category: :remote_development do
+ describe 'usage of Result.ok and Result.err' do
+ context 'when checked with .ok? and .err?' do
+ it 'works with ok result' do
+ result = Result.ok(:success)
+ expect(result.ok?).to eq(true)
+ expect(result.err?).to eq(false)
+ expect(result.unwrap).to eq(:success)
+ end
+
+ it 'works with error result' do
+ result = Result.err(:failure)
+ expect(result.err?).to eq(true)
+ expect(result.ok?).to eq(false)
+ expect(result.unwrap_err).to eq(:failure)
+ end
+ end
+
+ context 'when checked with destructuring' do
+ it 'works with ok result' do
+ Result.ok(:success) => { ok: } # example of rightward assignment
+ expect(ok).to eq(:success)
+
+ Result.ok(:success) => { ok: success_value } # rightward assignment destructuring to different var
+ expect(success_value).to eq(:success)
+ end
+
+ it 'works with error result' do
+ Result.err(:failure) => { err: }
+ expect(err).to eq(:failure)
+
+ Result.err(:failure) => { err: error_value }
+ expect(error_value).to eq(:failure)
+ end
+ end
+
+ context 'when checked with pattern matching' do
+ def check_result_with_pattern_matching(result)
+ case result
+ in { ok: Symbol => ok_value }
+ { success: ok_value }
+ in { err: String => error_value }
+ { failure: error_value }
+ else
+ raise "Unmatched result type: #{result.unwrap.class.name}"
+ end
+ end
+
+ it 'works with ok result' do
+ ok_result = Result.ok(:success_symbol)
+ expect(check_result_with_pattern_matching(ok_result)).to eq({ success: :success_symbol })
+ end
+
+ it 'works with error result' do
+ error_result = Result.err('failure string')
+ expect(check_result_with_pattern_matching(error_result)).to eq({ failure: 'failure string' })
+ end
+
+ it 'raises error with unmatched type in pattern match' do
+ unmatched_type_result = Result.ok([])
+ expect do
+ check_result_with_pattern_matching(unmatched_type_result)
+ end.to raise_error(RuntimeError, 'Unmatched result type: Array')
+ end
+
+ it 'raises error with invalid pattern matching key' do
+ result = Result.ok(:success)
+ expect do
+ case result
+ in { invalid_pattern_match_because_it_is_not_ok_or_err: :value }
+ :unreachable_from_case
+ else
+ :unreachable_from_else
+ end
+ end.to raise_error(ArgumentError, 'Use either :ok or :err for pattern matching')
+ end
+ end
+ end
+
+ describe 'usage of #and_then' do
+ context 'when passed a proc' do
+ it 'returns last ok value in successful chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .and_then(->(value) { Result.ok(value + 1) })
+ .and_then(->(value) { Result.ok(value + 1) })
+
+ expect(final_result.ok?).to eq(true)
+ expect(final_result.unwrap).to eq(3)
+ end
+
+ it 'short-circuits the rest of the chain on the first err value encountered' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .and_then(->(value) { Result.err("invalid: #{value}") })
+ .and_then(->(value) { Result.ok(value + 1) })
+
+ expect(final_result.err?).to eq(true)
+ expect(final_result.unwrap_err).to eq('invalid: 1')
+ end
+ end
+
+ context 'when passed a module or class (singleton) method object' do
+ module MyModuleUsingResult
+ def self.double(value)
+ Result.ok(value * 2)
+ end
+
+ def self.return_err(value)
+ Result.err("invalid: #{value}")
+ end
+
+ class MyClassUsingResult
+ def self.triple(value)
+ Result.ok(value * 3)
+ end
+ end
+ end
+
+ it 'returns last ok value in successful chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .and_then(::MyModuleUsingResult.method(:double))
+ .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))
+
+ expect(final_result.ok?).to eq(true)
+ expect(final_result.unwrap).to eq(6)
+ end
+
+ it 'returns first err value in failed chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .and_then(::MyModuleUsingResult.method(:double))
+ .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))
+ .and_then(::MyModuleUsingResult.method(:return_err))
+ .and_then(::MyModuleUsingResult.method(:double))
+
+ expect(final_result.err?).to eq(true)
+ expect(final_result.unwrap_err).to eq('invalid: 6')
+ end
+ end
+
+ describe 'type checking validation' do
+ describe 'enforcement of argument type' do
+ it 'raises TypeError if passed anything other than a lambda or singleton method object' do
+ ex = TypeError
+ msg = /expects a lambda or singleton method object/
+ # noinspection RubyMismatchedArgumentType
+ expect { Result.ok(1).and_then('string') }.to raise_error(ex, msg)
+ expect { Result.ok(1).and_then(proc { Result.ok(1) }) }.to raise_error(ex, msg)
+ expect { Result.ok(1).and_then(1.method(:to_s)) }.to raise_error(ex, msg)
+ expect { Result.ok(1).and_then(Integer.method(:to_s)) }.to raise_error(ex, msg)
+ end
+ end
+
+ describe 'enforcement of argument arity' do
+ it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
+ expect do
+ Result.ok(1).and_then(->(a, b) { Result.ok(a + b) })
+ end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
+ end
+ end
+
+ describe 'enforcement that passed lambda or method returns a Result type' do
+ it 'raises ArgumentError if passed lambda or singleton method object which returns non-Result type' do
+ expect do
+ Result.ok(1).and_then(->(a) { a + 1 })
+ end.to raise_error(TypeError, /expects .* which returns a 'Result' type/)
+ end
+ end
+ end
+ end
+
+ describe 'usage of #map' do
+ context 'when passed a proc' do
+ it 'returns last ok value in successful chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .map(->(value) { value + 1 })
+ .map(->(value) { value + 1 })
+
+ expect(final_result.ok?).to eq(true)
+ expect(final_result.unwrap).to eq(3)
+ end
+
+ it 'returns first err value in failed chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .and_then(->(value) { Result.err("invalid: #{value}") })
+ .map(->(value) { value + 1 })
+
+ expect(final_result.err?).to eq(true)
+ expect(final_result.unwrap_err).to eq('invalid: 1')
+ end
+ end
+
+ context 'when passed a module or class (singleton) method object' do
+ module MyModuleNotUsingResult
+ def self.double(value)
+ value * 2
+ end
+
+ class MyClassNotUsingResult
+ def self.triple(value)
+ value * 3
+ end
+ end
+ end
+
+ it 'returns last ok value in successful chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .map(::MyModuleNotUsingResult.method(:double))
+ .map(::MyModuleNotUsingResult::MyClassNotUsingResult.method(:triple))
+
+ expect(final_result.ok?).to eq(true)
+ expect(final_result.unwrap).to eq(6)
+ end
+
+ it 'returns first err value in failed chain' do
+ initial_result = Result.ok(1)
+ final_result =
+ initial_result
+ .map(::MyModuleNotUsingResult.method(:double))
+ .and_then(->(value) { Result.err("invalid: #{value}") })
+ .map(::MyModuleUsingResult.method(:double))
+
+ expect(final_result.err?).to eq(true)
+ expect(final_result.unwrap_err).to eq('invalid: 2')
+ end
+ end
+
+ describe 'type checking validation' do
+ describe 'enforcement of argument type' do
+ it 'raises TypeError if passed anything other than a lambda or singleton method object' do
+ ex = TypeError
+ msg = /expects a lambda or singleton method object/
+ # noinspection RubyMismatchedArgumentType
+ expect { Result.ok(1).map('string') }.to raise_error(ex, msg)
+ expect { Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg)
+ expect { Result.ok(1).map(1.method(:to_s)) }.to raise_error(ex, msg)
+ expect { Result.ok(1).map(Integer.method(:to_s)) }.to raise_error(ex, msg)
+ end
+ end
+
+ describe 'enforcement of argument arity' do
+ it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
+ expect do
+ Result.ok(1).map(->(a, b) { a + b })
+ end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
+ end
+ end
+
+ describe 'enforcement that passed lambda or method does not return a Result type' do
+ it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do
+ expect do
+ Result.ok(1).map(->(a) { Result.ok(a + 1) })
+ end.to raise_error(TypeError, /expects .* which returns an unwrapped value, not a 'Result'/)
+ end
+ end
+ end
+ end
+
+ describe '#unwrap' do
+ it 'returns wrapped value if ok' do
+ expect(Result.ok(1).unwrap).to eq(1)
+ end
+
+ it 'raises error if err' do
+ expect { Result.err('error').unwrap }.to raise_error(RuntimeError, /called.*unwrap.*on an 'err' Result/i)
+ end
+ end
+
+ describe '#unwrap_err' do
+ it 'returns wrapped value if err' do
+ expect(Result.err('error').unwrap_err).to eq('error')
+ end
+
+ it 'raises error if ok' do
+ expect { Result.ok(1).unwrap_err }.to raise_error(RuntimeError, /called.*unwrap_err.*on an 'ok' Result/i)
+ end
+ end
+
+ describe '#==' do
+ it 'implements equality' do
+ expect(Result.ok(1)).to eq(Result.ok(1))
+ expect(Result.err('error')).to eq(Result.err('error'))
+ expect(Result.ok(1)).not_to eq(Result.ok(2))
+ expect(Result.err('error')).not_to eq(Result.err('other error'))
+ expect(Result.ok(1)).not_to eq(Result.err(1))
+ end
+ end
+
+ describe 'validation' do
+ context 'for enforcing usage of only public interface' do
+ context 'when private constructor is called with invalid params' do
+ it 'raises ArgumentError if both ok_value and err_value are passed' do
+ expect { Result.new(ok_value: :ignored, err_value: :ignored) }
+ .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
+ end
+
+ it 'raises ArgumentError if neither ok_value nor err_value are passed' do
+ expect { Result.new }
+ .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
+ end
+ end
+ end
+ end
+end
+# rubocop:enable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
diff --git a/spec/lib/search/navigation_spec.rb b/spec/lib/search/navigation_spec.rb
new file mode 100644
index 00000000000..da8c27b4990
--- /dev/null
+++ b/spec/lib/search/navigation_spec.rb
@@ -0,0 +1,280 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Search::Navigation, feature_category: :global_search do
+ let(:user) { instance_double(User) }
+ let(:project_double) { instance_double(Project) }
+ let(:options) { {} }
+ let(:search_navigation) { described_class.new(user: user, project: project, options: options) }
+
+ describe '#tab_enabled_for_project?' do
+ let(:project) { project_double }
+ let(:tab) { :blobs }
+
+ subject(:tab_enabled_for_project) { search_navigation.tab_enabled_for_project?(tab) }
+
+ context 'when user has ability for tab' do
+ before do
+ allow(search_navigation).to receive(:can?).with(user, :read_code, project_double).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when user does not have ability for tab' do
+ before do
+ allow(search_navigation).to receive(:can?).with(user, :read_code, project_double).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when an array of projects is provided' do
+ let(:project) { Array.wrap(project_double) }
+
+ before do
+ allow(search_navigation).to receive(:can?).with(user, :read_code, project_double).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when project is not present' do
+ let_it_be(:project) { nil }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#tabs' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow(search_navigation).to receive(:can?).and_return(true)
+ allow(search_navigation).to receive(:tab_enabled_for_project?).and_return(false)
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?).and_return(false)
+ end
+
+ subject(:tabs) { search_navigation.tabs }
+
+ context 'for projects tab' do
+ where(:project, :condition) do
+ nil | true
+ ref(:project_double) | false
+ end
+
+ with_them do
+ it 'data item condition is set correctly' do
+ expect(tabs[:projects][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for code tab' do
+ where(:feature_flag_enabled, :show_elasticsearch_tabs, :project, :tab_enabled, :condition) do
+ false | false | nil | false | false
+ true | true | nil | true | true
+ true | false | nil | false | false
+ false | true | nil | false | false
+ false | false | ref(:project_double) | true | true
+ true | false | ref(:project_double) | false | false
+ end
+
+ with_them do
+ let(:options) { { show_elasticsearch_tabs: show_elasticsearch_tabs } }
+
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_code_tab).and_return(feature_flag_enabled)
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:blobs).and_return(tab_enabled)
+
+ expect(tabs[:blobs][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for issues tab' do
+ where(:tab_enabled, :feature_flag_enabled, :project, :condition) do
+ false | false | nil | false
+ false | true | nil | true
+ false | true | ref(:project_double) | false
+ false | false | ref(:project_double) | false
+ true | false | nil | true
+ true | true | nil | true
+ true | false | ref(:project_double) | true
+ true | true | ref(:project_double) | true
+ end
+
+ with_them do
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_issues_tab).and_return(feature_flag_enabled)
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:issues).and_return(tab_enabled)
+
+ expect(tabs[:issues][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for merge requests tab' do
+ where(:tab_enabled, :feature_flag_enabled, :project, :condition) do
+ false | false | nil | false
+ true | false | nil | true
+ false | false | ref(:project_double) | false
+ true | false | ref(:project_double) | true
+ false | true | nil | true
+ true | true | nil | true
+ false | true | ref(:project_double) | false
+ true | true | ref(:project_double) | true
+ end
+
+ with_them do
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_merge_requests_tab).and_return(feature_flag_enabled)
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:merge_requests).and_return(tab_enabled)
+
+ expect(tabs[:merge_requests][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for wiki tab' do
+ where(:feature_flag_enabled, :show_elasticsearch_tabs, :project, :tab_enabled, :condition) do
+ false | false | nil | true | true
+ false | false | nil | false | false
+ false | false | ref(:project_double) | false | false
+ false | true | nil | false | false
+ false | true | ref(:project_double) | false | false
+ true | false | nil | false | false
+ true | true | ref(:project_double) | false | false
+ end
+
+ with_them do
+ let(:options) { { show_elasticsearch_tabs: show_elasticsearch_tabs } }
+
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_wiki_tab).and_return(feature_flag_enabled)
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:wiki_blobs).and_return(tab_enabled)
+
+ expect(tabs[:wiki_blobs][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for commits tab' do
+ where(:feature_flag_enabled, :show_elasticsearch_tabs, :project, :tab_enabled, :condition) do
+ false | false | nil | true | true
+ false | false | ref(:project_double) | true | true
+ false | false | nil | false | false
+ false | true | ref(:project_double) | false | false
+ false | true | nil | false | false
+ true | false | nil | false | false
+ true | false | ref(:project_double) | false | false
+ true | true | ref(:project_double) | false | false
+ true | true | nil | false | true
+ end
+
+ with_them do
+ let(:options) { { show_elasticsearch_tabs: show_elasticsearch_tabs } }
+
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_commits_tab).and_return(feature_flag_enabled)
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:commits).and_return(tab_enabled)
+
+ expect(tabs[:commits][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for comments tab' do
+ where(:tab_enabled, :show_elasticsearch_tabs, :project, :condition) do
+ true | true | nil | true
+ true | true | ref(:project_double) | true
+ false | false | nil | false
+ false | false | ref(:project_double) | false
+ false | true | nil | true
+ false | true | ref(:project_double) | false
+ true | false | nil | true
+ true | false | ref(:project_double) | true
+ end
+
+ with_them do
+ let(:options) { { show_elasticsearch_tabs: show_elasticsearch_tabs } }
+
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:notes).and_return(tab_enabled)
+
+ expect(tabs[:notes][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for milestones tab' do
+ where(:project, :tab_enabled, :condition) do
+ ref(:project_double) | true | true
+ nil | false | true
+ ref(:project_double) | false | false
+ nil | true | true
+ end
+
+ with_them do
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:milestones).and_return(tab_enabled)
+
+ expect(tabs[:milestones][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for users tab' do
+ where(:feature_flag_enabled, :can_read_users_list, :project, :tab_enabled, :condition) do
+ false | false | ref(:project_double) | true | true
+ false | false | nil | false | false
+ false | true | nil | false | false
+ false | true | ref(:project_double) | false | false
+ true | true | nil | false | true
+ true | true | ref(:project_double) | false | false
+ end
+
+ with_them do
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:tab_enabled_for_project?).with(:users).and_return(tab_enabled)
+ allow(search_navigation).to receive(:can?)
+ .with(user, :read_users_list, project_double).and_return(can_read_users_list)
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_users_tab).and_return(feature_flag_enabled)
+
+ expect(tabs[:users][:condition]).to eq(condition)
+ end
+ end
+ end
+
+ context 'for snippet_titles tab' do
+ where(:project, :show_snippets, :feature_flag_enabled, :condition) do
+ ref(:project_double) | true | false | false
+ nil | false | false | false
+ ref(:project_double) | false | false | false
+ nil | true | false | false
+ ref(:project_double) | true | true | false
+ nil | false | true | false
+ ref(:project_double) | false | true | false
+ nil | true | true | true
+ end
+
+ with_them do
+ let(:options) { { show_snippets: show_snippets } }
+
+ it 'data item condition is set correctly' do
+ allow(search_navigation).to receive(:feature_flag_tab_enabled?)
+ .with(:global_search_snippet_titles_tab).and_return(feature_flag_enabled)
+
+ expect(tabs[:snippet_titles][:condition]).to eq(condition)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/service_ping/devops_report_spec.rb b/spec/lib/service_ping/devops_report_spec.rb
index 793f3066097..bbd66390403 100644
--- a/spec/lib/service_ping/devops_report_spec.rb
+++ b/spec/lib/service_ping/devops_report_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ServicePing::DevopsReport do
let_it_be(:data) { { "conv_index": {} }.to_json }
- let_it_be(:subject) { ServicePing::DevopsReport.new(Gitlab::Json.parse(data)) }
+ let_it_be(:subject) { described_class.new(Gitlab::Json.parse(data)) }
let_it_be(:devops_report) { DevOpsReport::Metric.new }
describe '#execute' do
diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/analyze_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/analyze_menu_spec.rb
index 3d3d304a5a0..cc2809df85f 100644
--- a/spec/lib/sidebars/groups/super_sidebar_menus/analyze_menu_spec.rb
+++ b/spec/lib/sidebars/groups/super_sidebar_menus/analyze_menu_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::AnalyzeMenu, feature_categor
it 'defines list of NilMenuItem placeholders' do
expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem])
expect(items.map(&:item_id)).to eq([
+ :analytics_dashboards,
+ :dashboards_analytics,
:cycle_analytics,
:ci_cd_analytics,
:contribution_analytics,
diff --git a/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
new file mode 100644
index 00000000000..08fc352a6cd
--- /dev/null
+++ b/spec/lib/sidebars/organizations/menus/manage_menu_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Organizations::Menus::ManageMenu, feature_category: :navigation do
+ let_it_be(:organization) { build(:organization) }
+ let_it_be(:user) { build(:user) }
+ let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) }
+
+ let(:items) { subject.instance_variable_get(:@items) }
+
+ subject { described_class.new(context) }
+
+ it 'has title and sprite_icon' do
+ expect(subject.title).to eq(s_("Navigation|Manage"))
+ expect(subject.sprite_icon).to eq("users")
+ end
+
+ describe 'Menu items' do
+ subject { described_class.new(context).renderable_items.find { |e| e.item_id == item_id } }
+
+ describe 'Groups and projects' do
+ let(:item_id) { :organization_groups_and_projects }
+
+ it { is_expected.not_to be_nil }
+ end
+ end
+end
diff --git a/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb b/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb
new file mode 100644
index 00000000000..bc03787e95f
--- /dev/null
+++ b/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Organizations::Menus::ScopeMenu, feature_category: :navigation do
+ let_it_be(:organization) { build(:organization) }
+ let_it_be(:user) { build(:user) }
+ let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) }
+
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:menu) { described_class.new(context) }
+ let(:extra_attrs) do
+ {
+ title: s_('Organization|Organization overview'),
+ sprite_icon: 'organization',
+ super_sidebar_parent: ::Sidebars::StaticMenu,
+ item_id: :organization_overview
+ }
+ end
+ end
+end
diff --git a/spec/lib/sidebars/organizations/panel_spec.rb b/spec/lib/sidebars/organizations/panel_spec.rb
new file mode 100644
index 00000000000..1f0b8d72aef
--- /dev/null
+++ b/spec/lib/sidebars/organizations/panel_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Organizations::Panel, feature_category: :navigation do
+ let_it_be(:organization) { build(:organization) }
+ let_it_be(:user) { build(:user) }
+ let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) }
+
+ subject { described_class.new(context) }
+
+ it 'has a scope menu' do
+ expect(subject.scope_menu).to be_a(Sidebars::Organizations::Menus::ScopeMenu)
+ end
+
+ it_behaves_like 'a panel with uniquely identifiable menu items'
+end
diff --git a/spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb b/spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb
new file mode 100644
index 00000000000..99b33a5edf8
--- /dev/null
+++ b/spec/lib/sidebars/organizations/super_sidebar_panel_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Organizations::SuperSidebarPanel, feature_category: :navigation do
+ let_it_be(:organization) { build(:organization) }
+ let_it_be(:user) { build(:user) }
+ let_it_be(:context) do
+ Sidebars::Context.new(
+ current_user: user,
+ container: organization
+ )
+ end
+
+ subject { described_class.new(context) }
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq(
+ {
+ title: organization.name,
+ id: organization.id
+ })
+ end
+
+ describe '#renderable_menus' do
+ let(:category_menu) do
+ [
+ Sidebars::StaticMenu,
+ Sidebars::Organizations::Menus::ManageMenu
+ ]
+ end
+
+ it "is exposed as a renderable menu" do
+ expect(subject.instance_variable_get(:@menus).map(&:class)).to eq(category_menu)
+ end
+ end
+
+ it_behaves_like 'a panel with uniquely identifiable menu items'
+end
diff --git a/spec/lib/sidebars/panel_spec.rb b/spec/lib/sidebars/panel_spec.rb
index 2c1b9c73595..857cb1139b5 100644
--- a/spec/lib/sidebars/panel_spec.rb
+++ b/spec/lib/sidebars/panel_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
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(:panel) { described_class.new(context) }
let(:menu1) { Sidebars::Menu.new(context) }
let(:menu2) { Sidebars::Menu.new(context) }
let(:menu3) { Sidebars::Menu.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
index a63acdb5dc2..75f612e9c7c 100644
--- a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
@@ -68,5 +68,31 @@ RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu, feature_category: :na
it_behaves_like 'access rights checks'
end
+
+ describe 'Pages' do
+ let(:item_id) { :pages }
+
+ before do
+ allow(project).to receive(:pages_available?).and_return(pages_enabled)
+ end
+
+ describe 'when pages are enabled' do
+ let(:pages_enabled) { true }
+
+ it { is_expected.not_to be_nil }
+
+ describe 'when the user does not have access' do
+ let(:user) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe 'when pages are not enabled' do
+ let(:pages_enabled) { false }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
index 363822ee5e4..c0787aa9db5 100644
--- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
@@ -7,7 +7,9 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu, feature_category: :naviga
let(:user) { project.first_owner }
let(:show_cluster_hint) { true }
- let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_cluster_hint: show_cluster_hint) }
+ let(:context) do
+ Sidebars::Projects::Context.new(current_user: user, container: project, show_cluster_hint: show_cluster_hint)
+ end
subject { described_class.new(context) }
@@ -86,5 +88,19 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu, feature_category: :naviga
it_behaves_like 'access rights checks'
end
+
+ describe 'Tracing' do
+ let(:item_id) { :tracing }
+
+ specify { is_expected.not_to be_nil }
+
+ describe 'when feature is disabled' do
+ before do
+ stub_feature_flags(observability_tracing: false)
+ end
+
+ specify { is_expected.to be_nil }
+ end
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index a60e46582f9..605cec8be5e 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -10,10 +10,6 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu, feature_category: :navig
subject { described_class.new(context) }
- before do
- stub_feature_flags(show_pages_in_deployments_menu: false)
- end
-
describe '#render?' do
it 'returns false when menu does not have any menu items' do
allow(subject).to receive(:has_renderable_items?).and_return(false)
@@ -117,32 +113,6 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu, feature_category: :navig
end
end
- describe 'Pages' do
- let(:item_id) { :pages }
-
- before do
- allow(project).to receive(:pages_available?).and_return(pages_enabled)
- end
-
- describe 'when pages are enabled' do
- let(:pages_enabled) { true }
-
- specify { is_expected.not_to be_nil }
-
- describe 'when the user does not have access' do
- let(:user) { nil }
-
- specify { is_expected.to be_nil }
- end
- end
-
- describe 'when pages are not enabled' do
- let(:pages_enabled) { false }
-
- specify { is_expected.to be_nil }
- end
- end
-
describe 'Merge requests' do
let(:item_id) { :merge_requests }
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb
index b7d05867d77..d459d47c31a 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb
@@ -23,7 +23,8 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu, feature_categ
:code_review,
:merge_request_analytics,
:issues,
- :insights
+ :insights,
+ :model_experiments
])
end
end
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb
index 50eee173d31..98d62948ac3 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb
@@ -18,8 +18,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::DeployMenu, feature_catego
:releases,
:feature_flags,
:packages_registry,
- :container_registry,
- :model_experiments
+ :container_registry
])
end
end
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
index e59062c7eaf..e5c5204e0b4 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::MonitorMenu, feature_categ
it 'defines list of NilMenuItem placeholders' do
expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem])
expect(items.map(&:item_id)).to eq([
+ :tracing,
:error_tracking,
:alert_management,
:incidents,
diff --git a/spec/lib/slack/manifest_spec.rb b/spec/lib/slack/manifest_spec.rb
new file mode 100644
index 00000000000..d4fdea80ff5
--- /dev/null
+++ b/spec/lib/slack/manifest_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Slack::Manifest, feature_category: :integrations do
+ describe '.to_h' do
+ it 'creates the correct manifest' do
+ expect(described_class.to_h).to eq({
+ display_information: {
+ name: "GitLab (#{Gitlab.config.gitlab.host})",
+ description: s_('SlackIntegration|Interact with GitLab without leaving your Slack workspace!'),
+ background_color: '#171321',
+ long_description: "Generated for #{Gitlab.config.gitlab.host} by GitLab #{Gitlab::VERSION}.\r\n\r\n" \
+ "- *Notifications:* Get notifications to your team's Slack channel about events " \
+ "happening inside your GitLab projects.\r\n\r\n- *Slash commands:* Quickly open, " \
+ 'access, or close issues from Slack using the `/gitlab` command. Streamline your ' \
+ 'GitLab deployments with ChatOps.'
+ },
+ features: {
+ app_home: {
+ home_tab_enabled: true,
+ messages_tab_enabled: false,
+ messages_tab_read_only_enabled: true
+ },
+ bot_user: {
+ display_name: 'GitLab',
+ always_online: true
+ },
+ slash_commands: [
+ {
+ command: '/gitlab',
+ url: "#{Gitlab.config.gitlab.url}/api/v4/slack/trigger",
+ description: 'GitLab slash commands',
+ usage_hint: 'your-project-name-or-alias command',
+ should_escape: false
+ }
+ ]
+ },
+ oauth_config: {
+ redirect_urls: [
+ Gitlab.config.gitlab.url
+ ],
+ scopes: {
+ bot: %w[
+ commands
+ chat:write
+ chat:write.public
+ ]
+ }
+ },
+ settings: {
+ event_subscriptions: {
+ request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/events",
+ bot_events: %w[
+ app_home_opened
+ ]
+ },
+ interactivity: {
+ is_enabled: true,
+ request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/interactions",
+ message_menu_options_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/options"
+ },
+ org_deploy_enabled: false,
+ socket_mode_enabled: false,
+ token_rotation_enabled: false
+ }
+ })
+ end
+ end
+
+ describe '.to_json' do
+ subject(:to_json) { described_class.to_json }
+
+ shared_examples 'a manifest that matches the JSON schema' do
+ # JSON schema file downloaded from
+ # https://raw.githubusercontent.com/slackapi/manifest-schema/v0.0.0/schemas/manifest.schema.2.0.0.json
+ # via https://github.com/slackapi/manifest-schema.
+ it { is_expected.to match_schema('slack/manifest') }
+ end
+
+ it_behaves_like 'a manifest that matches the JSON schema'
+
+ context 'when the host name is very long' do
+ before do
+ allow(Gitlab.config.gitlab).to receive(:host).and_return('abc' * 20)
+ end
+
+ it_behaves_like 'a manifest that matches the JSON schema'
+ end
+ end
+
+ describe '.share_url' do
+ it 'URI encodes the manifest' do
+ allow(described_class).to receive(:to_h).and_return({ foo: 'bar' })
+
+ expect(described_class.share_url).to eq('https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D')
+ end
+ end
+end
diff --git a/spec/mailers/emails/in_product_marketing_spec.rb b/spec/mailers/emails/in_product_marketing_spec.rb
index 5419c9e6798..2d332dd99d6 100644
--- a/spec/mailers/emails/in_product_marketing_spec.rb
+++ b/spec/mailers/emails/in_product_marketing_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe Emails::InProductMarketing do
is_expected.to have_body_text(message.subtitle)
is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
- if track =~ /(create|verify)/
+ if /create|verify/.match?(track)
is_expected.to have_body_text(message.invite_text)
is_expected.to have_body_text(CGI.unescapeHTML(message.invite_link))
else
diff --git a/spec/mailers/emails/issues_spec.rb b/spec/mailers/emails/issues_spec.rb
index b5f3972f38e..b35e83dfb75 100644
--- a/spec/mailers/emails/issues_spec.rb
+++ b/spec/mailers/emails/issues_spec.rb
@@ -36,6 +36,15 @@ RSpec.describe Emails::Issues, feature_category: :team_planning do
expect(subject).to have_body_text "23, 34, 58"
end
+ it "shows issuable errors with column" do
+ @results = { success: 0, error_lines: [], parse_error: false,
+ preprocess_errors:
+ { milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 15.11] } } } }
+
+ expect(subject).to have_body_text "Could not find the following milestone values in \
+#{project.full_name}: 15.10, 15.11"
+ end
+
context 'with header and footer' do
let(:results) { { success: 165, error_lines: [], parse_error: false } }
diff --git a/spec/mailers/emails/projects_spec.rb b/spec/mailers/emails/projects_spec.rb
index ef3c21b32ce..1f0f09f7ca2 100644
--- a/spec/mailers/emails/projects_spec.rb
+++ b/spec/mailers/emails/projects_spec.rb
@@ -104,7 +104,6 @@ RSpec.describe Emails::Projects do
let_it_be(:environment) { create(:environment, project: project) }
let(:payload) { { 'gitlab_environment_name' => environment.name } }
- let(:metrics_url) { metrics_project_environment_url(project, environment) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
@@ -131,7 +130,6 @@ RSpec.describe Emails::Projects do
let(:alert) { create(:alert_management_alert, :prometheus, :from_payload, payload: payload, project: project) }
let(:title) { "#{prometheus_alert.title} #{prometheus_alert.computed_operator} #{prometheus_alert.threshold}" }
- let(:metrics_url) { metrics_project_environment_url(project, environment) }
before do
payload['labels'] = {
@@ -157,7 +155,6 @@ RSpec.describe Emails::Projects do
is_expected.to have_body_text(environment.name)
is_expected.to have_body_text('Metric:')
is_expected.to have_body_text(prometheus_alert.full_query)
- is_expected.to have_body_text(metrics_url)
is_expected.not_to have_body_text('Description:')
end
end
diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb
index e8ca9533256..4c92762e624 100644
--- a/spec/mailers/emails/releases_spec.rb
+++ b/spec/mailers/emails/releases_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe Emails::Releases do
let(:release) { create(:release, project: project, description: "Attachment: [Test file](#{upload_path})") }
it 'renders absolute links' do
- is_expected.to have_body_text(%Q(<a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">Test file</a>))
+ is_expected.to have_body_text(%(<a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">Test file</a>))
end
end
end
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index 22b910b3dae..8c0efe3f480 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -340,8 +340,8 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
end
end
- let_it_be(:expected_html) { %Q(a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
- let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
+ let_it_be(:expected_html) { %(a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
+ let_it_be(:expected_template_html) { %(some text #{expected_html}) }
it_behaves_like 'a service desk notification email'
it_behaves_like 'a service desk notification email with template content', 'new_note'
@@ -357,7 +357,7 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
end
context 'when upload name is not changed in markdown' do
- let_it_be(:expected_template_html) { %Q(some text a new comment with <strong>#{filename}</strong>) }
+ let_it_be(:expected_template_html) { %(some text a new comment with <strong>#{filename}</strong>) }
it_behaves_like 'a service desk notification email', 1
it_behaves_like 'a service desk notification email with template content', 'new_note', 1
@@ -366,9 +366,9 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
context 'when upload name is changed in markdown' do
let_it_be(:upload_name_in_markdown) { 'Custom name' }
let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [#{upload_name_in_markdown}](#{upload_path})") }
- let_it_be(:expected_text) { %Q(a new comment with [#{upload_name_in_markdown}](#{upload_path})) }
- let_it_be(:expected_html) { %Q(a new comment with <strong>#{upload_name_in_markdown} (#{filename})</strong>) }
- let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
+ let_it_be(:expected_text) { %(a new comment with [#{upload_name_in_markdown}](#{upload_path})) }
+ let_it_be(:expected_html) { %(a new comment with <strong>#{upload_name_in_markdown} (#{filename})</strong>) }
+ let_it_be(:expected_template_html) { %(some text #{expected_html}) }
it_behaves_like 'a service desk notification email', 1
it_behaves_like 'a service desk notification email with template content', 'new_note', 1
@@ -392,16 +392,16 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
let_it_be(:upload_1) { create(:upload, :issuable_upload, :with_file, model: note.project, path: path_1, secret: secret_1) }
- let_it_be(:expected_html) { %Q(a new comment with <strong>#{filename}</strong> <strong>#{filename_1}</strong>) }
- let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
+ let_it_be(:expected_html) { %(a new comment with <strong>#{filename}</strong> <strong>#{filename_1}</strong>) }
+ let_it_be(:expected_template_html) { %(some text #{expected_html}) }
it_behaves_like 'a service desk notification email', 2
it_behaves_like 'a service desk notification email with template content', 'new_note', 2
end
context 'when not all uploads processed correct' do
- let_it_be(:expected_html) { %Q(a new comment with <strong>#{filename}</strong> <a href="#{project.web_url}#{upload_path_1}" data-canonical-src="#{upload_path_1}" data-link="true" class="gfm">#{filename_1}</a>) }
- let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
+ let_it_be(:expected_html) { %(a new comment with <strong>#{filename}</strong> <a href="#{project.web_url}#{upload_path_1}" data-canonical-src="#{upload_path_1}" data-link="true" class="gfm">#{filename_1}</a>) }
+ let_it_be(:expected_template_html) { %(some text #{expected_html}) }
it_behaves_like 'a service desk notification email', 1
it_behaves_like 'a service desk notification email with template content', 'new_note', 1
@@ -417,7 +417,7 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(StandardError, project_id: note.project_id)
end
- let_it_be(:expected_template_html) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
+ let_it_be(:expected_template_html) { %(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
it_behaves_like 'a service desk notification email with template content', 'new_note'
end
@@ -430,7 +430,7 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(StandardError, project_id: note.project_id)
end
- let_it_be(:expected_template_html) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
+ let_it_be(:expected_template_html) { %(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
it_behaves_like 'a service desk notification email with template content', 'new_note'
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 372808b64d3..629dfdaf55e 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -109,8 +109,14 @@ RSpec.describe Notify do
is_expected.to have_body_text issue.description
end
- it 'does not add a reason header' do
- is_expected.not_to have_header('X-GitLab-NotificationReason', /.+/)
+ context 'when issue is confidential' do
+ before do
+ issue.update_attribute(:confidential, true)
+ end
+
+ it 'has a confidential header set to true' do
+ is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true')
+ end
end
context 'when sent with a reason' do
@@ -819,6 +825,10 @@ RSpec.describe Notify do
let_it_be(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note, project: project) }
let_it_be(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note, project: project) }
+ before_all do
+ first_note.noteable.update_attribute(:confidential, "true")
+ end
+
subject { described_class.note_issue_email(recipient.id, third_note.id) }
it_behaves_like 'an email sent to a user'
@@ -840,17 +850,29 @@ RSpec.describe Notify do
it 'has X-GitLab-Discussion-ID header' do
expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id)
end
+
+ it 'has a confidential header set to true' do
+ is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true')
+ end
end
context 'individual issue comments' do
let_it_be(:note) { create(:note_on_issue, project: project) }
+ before_all do
+ note.noteable.update_attribute(:confidential, "true")
+ end
+
subject { described_class.note_issue_email(recipient.id, note.id) }
it_behaves_like 'an email sent to a user'
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
+ it 'has a confidential header set to true' do
+ expect(subject.header['X-GitLab-ConfidentialIssue'].value).to eq('true')
+ end
+
it 'has In-Reply-To header pointing to the issue' do
expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"])
end
@@ -1572,16 +1594,22 @@ RSpec.describe Notify do
context 'when custom email is enabled' do
let_it_be(:credentials) { create(:service_desk_custom_email_credential, project: project) }
+ let_it_be(:verification) { create(:service_desk_custom_email_verification, project: project) }
let_it_be(:settings) do
create(
:service_desk_setting,
project: project,
- custom_email_enabled: true,
custom_email: 'supersupport@example.com'
)
end
+ before_all do
+ verification.mark_as_finished!
+ project.reset
+ settings.update!(custom_email_enabled: true)
+ end
+
it 'uses custom email and service bot name in "from" header' do
expect_sender(User.support_bot, sender_email: 'supersupport@example.com')
end
@@ -1630,22 +1658,23 @@ RSpec.describe Notify do
end
context 'when custom email is enabled' do
- let_it_be(:credentials) do
- create(
- :service_desk_custom_email_credential,
- project: project
- )
- end
+ let_it_be(:credentials) { create(:service_desk_custom_email_credential, project: project) }
+ let_it_be(:verification) { create(:service_desk_custom_email_verification, project: project) }
let_it_be(:settings) do
create(
:service_desk_setting,
project: project,
- custom_email_enabled: true,
custom_email: 'supersupport@example.com'
)
end
+ before_all do
+ verification.mark_as_finished!
+ project.reset
+ settings.update!(custom_email_enabled: true)
+ end
+
it 'uses custom email and author\'s name in "from" header' do
expect_sender(first_note.author, sender_email: project.service_desk_setting.custom_email)
end
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index ad80835549f..baf15a773b1 100644
--- a/spec/metrics_server/metrics_server_spec.rb
+++ b/spec/metrics_server/metrics_server_spec.rb
@@ -69,136 +69,29 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
end
describe '.spawn' do
- context 'for legacy Ruby server' do
- let(:expected_env) do
- {
- 'METRICS_SERVER_TARGET' => target,
- 'WIPE_METRICS_DIR' => '0',
- 'GITLAB_CONFIG' => 'path/to/config/gitlab.yml'
- }
- end
-
- before do
- stub_env('GITLAB_CONFIG', 'path/to/config/gitlab.yml')
- end
-
- it 'spawns a new server process and returns its PID' do
- expect(Process).to receive(:spawn).with(
- expected_env,
- end_with('bin/metrics-server'),
- hash_including(pgroup: true)
- ).and_return(99)
- expect(Process).to receive(:detach).with(99)
-
- pid = described_class.spawn(target, metrics_dir: metrics_dir)
-
- expect(pid).to eq(99)
- end
+ let(:expected_env) do
+ {
+ 'METRICS_SERVER_TARGET' => target,
+ 'WIPE_METRICS_DIR' => '0',
+ 'GITLAB_CONFIG' => 'path/to/config/gitlab.yml'
+ }
end
- context 'for Golang server' do
- let(:log_enabled) { false }
- let(:settings) do
- GitlabSettings::Options.build(
- {
- 'web_exporter' => {
- 'enabled' => true,
- 'address' => 'localhost',
- 'port' => '8083',
- 'log_enabled' => log_enabled
- },
- 'sidekiq_exporter' => {
- 'enabled' => true,
- 'address' => 'localhost',
- 'port' => '8082',
- 'log_enabled' => log_enabled
- }
- }
- )
- end
-
- let(:expected_port) { target == 'puma' ? '8083' : '8082' }
- let(:expected_env) do
- {
- 'GOGC' => '10',
- 'GME_MMAP_METRICS_DIR' => metrics_dir,
- 'GME_PROBES' => 'self,mmap,mmap_stats',
- 'GME_SERVER_HOST' => 'localhost',
- 'GME_SERVER_PORT' => expected_port,
- 'GME_LOG_LEVEL' => 'quiet'
- }
- end
-
- before do
- stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
- allow(::Settings).to receive(:monitoring).and_return(settings)
- end
-
- it 'spawns a new server process and returns its PID' do
- expect(Process).to receive(:spawn).with(
- expected_env,
- 'gitlab-metrics-exporter',
- hash_including(pgroup: true)
- ).and_return(99)
- expect(Process).to receive(:detach).with(99)
-
- pid = described_class.spawn(target, metrics_dir: metrics_dir)
-
- expect(pid).to eq(99)
- end
-
- it 'can launch from explicit path instead of PATH' do
- expect(Process).to receive(:spawn).with(
- expected_env,
- '/path/to/gme/gitlab-metrics-exporter',
- hash_including(pgroup: true)
- ).and_return(99)
-
- described_class.spawn(target, metrics_dir: metrics_dir, path: '/path/to/gme/')
- end
+ before do
+ stub_env('GITLAB_CONFIG', 'path/to/config/gitlab.yml')
+ end
- context 'when logs are enabled' do
- let(:log_enabled) { true }
- let(:expected_log_file) { target == 'puma' ? 'web_exporter.log' : 'sidekiq_exporter.log' }
-
- it 'sets log related environment variables' do
- expect(Process).to receive(:spawn).with(
- expected_env.merge(
- 'GME_LOG_LEVEL' => 'info',
- 'GME_LOG_FILE' => File.join(Rails.root, 'log', expected_log_file)
- ),
- 'gitlab-metrics-exporter',
- hash_including(pgroup: true)
- ).and_return(99)
-
- described_class.spawn(target, metrics_dir: metrics_dir)
- end
- end
+ it 'spawns a new server process and returns its PID' do
+ expect(Process).to receive(:spawn).with(
+ expected_env,
+ end_with('bin/metrics-server'),
+ hash_including(pgroup: true)
+ ).and_return(99)
+ expect(Process).to receive(:detach).with(99)
- context 'when TLS settings are present' do
- before do
- settings.web_exporter['tls_enabled'] = true
- settings.web_exporter['tls_cert_path'] = '/path/to/cert.pem'
- settings.web_exporter['tls_key_path'] = '/path/to/key.pem'
+ pid = described_class.spawn(target, metrics_dir: metrics_dir)
- settings.sidekiq_exporter['tls_enabled'] = true
- settings.sidekiq_exporter['tls_cert_path'] = '/path/to/cert.pem'
- settings.sidekiq_exporter['tls_key_path'] = '/path/to/key.pem'
- end
-
- it 'sets the correct environment variables' do
- expect(Process).to receive(:spawn).with(
- expected_env.merge(
- 'GME_CERT_FILE' => '/path/to/cert.pem',
- 'GME_CERT_KEY' => '/path/to/key.pem'
- ),
- '/path/to/gme/gitlab-metrics-exporter',
- hash_including(pgroup: true)
- ).and_return(99)
-
- described_class.spawn(target, metrics_dir: metrics_dir, path: '/path/to/gme/')
- end
- end
+ expect(pid).to eq(99)
end
end
end
@@ -214,21 +107,10 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
end
describe '.spawn' do
- context 'for legacy Ruby server' do
- it 'raises an error' do
- expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
- raise_error('Target must be one of [puma,sidekiq]')
- )
- end
- end
-
- context 'for Golang server' do
- it 'raises an error' do
- stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
- expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
- raise_error('Target must be one of [puma,sidekiq]')
- )
- end
+ it 'raises an error' do
+ expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
+ raise_error('Target must be one of [puma,sidekiq]')
+ )
end
end
end
@@ -345,21 +227,10 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
end
describe '.start_for_sidekiq' do
- context 'for legacy Ruby server' do
- it 'forks the parent process' do
- expect(Process).to receive(:fork).and_return(42)
+ it 'forks the parent process' do
+ expect(Process).to receive(:fork).and_return(42)
- described_class.start_for_sidekiq(metrics_dir: '/path/to/metrics')
- end
- end
-
- context 'for Golang server' do
- it 'spawns the server process' do
- stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
- expect(Process).to receive(:spawn).and_return(42)
-
- described_class.start_for_sidekiq(metrics_dir: '/path/to/metrics')
- end
+ described_class.start_for_sidekiq(metrics_dir: '/path/to/metrics')
end
end
diff --git a/spec/migrations/20230523101514_finalize_user_type_migration_spec.rb b/spec/migrations/20230523101514_finalize_user_type_migration_spec.rb
index abf3a506748..01c05c38098 100644
--- a/spec/migrations/20230523101514_finalize_user_type_migration_spec.rb
+++ b/spec/migrations/20230523101514_finalize_user_type_migration_spec.rb
@@ -5,7 +5,14 @@ require_migration!
RSpec.describe FinalizeUserTypeMigration, feature_category: :devops_reports do
it 'finalizes MigrateHumanUserType migration' do
- expect(described_class).to be_finalize_background_migration_of('MigrateHumanUserType')
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(
+ job_class_name: 'MigrateHumanUserType',
+ table_name: :users,
+ column_name: :id,
+ job_arguments: []
+ )
+ end
migrate!
end
diff --git a/spec/migrations/20230530012406_finalize_backfill_resource_link_events_spec.rb b/spec/migrations/20230530012406_finalize_backfill_resource_link_events_spec.rb
new file mode 100644
index 00000000000..0298b470ac8
--- /dev/null
+++ b/spec/migrations/20230530012406_finalize_backfill_resource_link_events_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinalizeBackfillResourceLinkEvents, feature_category: :team_planning do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+
+ context 'when migration is missing' do
+ before do
+ batched_migrations.where(job_class_name: described_class::MIGRATION).delete_all
+ end
+
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+ .once
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:batched_migration) do
+ batched_migrations.create!(
+ job_class_name: described_class::MIGRATION,
+ table_name: :system_note_metadata,
+ column_name: :id,
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 5,
+ batch_size: 5,
+ sub_batch_size: 5,
+ gitlab_schema: :gitlab_main,
+ status: status
+ )
+ end
+
+ context 'when migrations have finished' do
+ let(:status) { 3 } # finished enum value
+
+ it 'does not raise an error' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ it 'finalizes the migration' do
+ expect do
+ migrate!
+
+ batched_migration.reload
+ end.to change { batched_migration.status }.from(status).to(3)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230613192703_ensure_ci_build_needs_big_int_backfill_is_finished_for_self_hosts_spec.rb b/spec/migrations/20230613192703_ensure_ci_build_needs_big_int_backfill_is_finished_for_self_hosts_spec.rb
new file mode 100644
index 00000000000..0fe9cecb729
--- /dev/null
+++ b/spec/migrations/20230613192703_ensure_ci_build_needs_big_int_backfill_is_finished_for_self_hosts_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EnsureCiBuildNeedsBigIntBackfillIsFinishedForSelfHosts, migration: :gitlab_ci, feature_category: :continuous_integration do
+ describe '#up' do
+ let(:migration_arguments) do
+ {
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: 'ci_build_needs',
+ column_name: 'id',
+ job_arguments: [['id'], ['id_convert_to_bigint']]
+ }
+ end
+
+ it 'ensures the migration is completed' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)
+ end
+
+ migrate!
+ end
+ end
+end
diff --git a/spec/migrations/20230613192703_swap_ci_build_needs_to_big_int_for_self_hosts_spec.rb b/spec/migrations/20230613192703_swap_ci_build_needs_to_big_int_for_self_hosts_spec.rb
new file mode 100644
index 00000000000..3db5d3b3c16
--- /dev/null
+++ b/spec/migrations/20230613192703_swap_ci_build_needs_to_big_int_for_self_hosts_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapCiBuildNeedsToBigIntForSelfHosts, feature_category: :continuous_integration do
+ after do
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE ci_build_needs DROP COLUMN IF EXISTS id_convert_to_bigint')
+ end
+
+ describe '#up' do
+ context 'when on GitLab.com, dev, or test' do
+ before do
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id TYPE bigint')
+ connection.execute('ALTER TABLE ci_build_needs DROP COLUMN IF EXISTS id_convert_to_bigint')
+ end
+
+ it 'does not swap the columns' 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
+
+ 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('bigint')
+ expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
+ }
+
+ migration.after -> {
+ ci_build_needs.reset_column_information
+
+ 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' }).to be nil
+ }
+ end
+ end
+ end
+ end
+
+ context 'when a self-hosted installation has already completed the swap' do
+ before do
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id TYPE bigint')
+ connection.execute('ALTER TABLE ci_build_needs ADD COLUMN IF NOT EXISTS id_convert_to_bigint integer')
+ end
+
+ it 'does not swap the columns' 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
+
+ ci_build_needs = table(:ci_build_needs)
+
+ migrate!
+
+ expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ expect(ci_build_needs.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('integer')
+ end
+ end
+
+ context 'when a self-hosted installation has the `id_convert_to_bigint` column already dropped' do
+ before do
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id TYPE bigint')
+ connection.execute('ALTER TABLE ci_build_needs DROP COLUMN IF EXISTS id_convert_to_bigint')
+ end
+
+ it 'does not swap the columns' 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
+
+ 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('bigint')
+ expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
+ }
+
+ migration.after -> {
+ ci_build_needs.reset_column_information
+
+ 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' }).to be nil
+ }
+ end
+ end
+ end
+ end
+
+ context 'when an installation is self-hosted' 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 ADD COLUMN IF NOT EXISTS id_convert_to_bigint bigint')
+ connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id_convert_to_bigint TYPE bigint')
+ connection.execute('DROP INDEX IF EXISTS index_ci_build_needs_on_id_convert_to_bigint')
+ connection.execute('CREATE OR REPLACE FUNCTION trigger_3207b8d0d6f3() RETURNS trigger LANGUAGE plpgsql AS $$
+ BEGIN NEW."id_convert_to_bigint" := NEW."id"; RETURN NEW; END; $$;')
+ end
+
+ it 'swaps the columns' 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
+
+ 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 do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ ci_build_needs.reset_column_information
+
+ expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ expect(ci_build_needs.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('integer')
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230616082958_add_unique_index_for_npm_packages_on_project_id_name_version_spec.rb b/spec/migrations/20230616082958_add_unique_index_for_npm_packages_on_project_id_name_version_spec.rb
new file mode 100644
index 00000000000..c7c97b16f97
--- /dev/null
+++ b/spec/migrations/20230616082958_add_unique_index_for_npm_packages_on_project_id_name_version_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddUniqueIndexForNpmPackagesOnProjectIdNameVersion, feature_category: :package_registry do
+ it 'schedules an index creation' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(ActiveRecord::Base.connection.indexes('packages_packages').map(&:name))
+ .not_to include('idx_packages_on_project_id_name_version_unique_when_npm')
+ }
+
+ migration.after -> {
+ expect(ActiveRecord::Base.connection.indexes('packages_packages').map(&:name))
+ .to include('idx_packages_on_project_id_name_version_unique_when_npm')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20230621070810_update_requeue_workers_in_application_settings_for_gitlab_com_spec.rb b/spec/migrations/20230621070810_update_requeue_workers_in_application_settings_for_gitlab_com_spec.rb
new file mode 100644
index 00000000000..763d9ea610c
--- /dev/null
+++ b/spec/migrations/20230621070810_update_requeue_workers_in_application_settings_for_gitlab_com_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateRequeueWorkersInApplicationSettingsForGitlabCom, feature_category: :global_search do
+ let(:settings) { table(:application_settings) }
+
+ describe "#up" do
+ it 'does nothing' do
+ record = settings.create!
+
+ expect { migrate! }.not_to change { record.reload.elasticsearch_requeue_workers }
+ end
+
+ it 'updates elasticsearch_requeue_workers when gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ record = settings.create!
+
+ expect { migrate! }.to change { record.reload.elasticsearch_requeue_workers }.from(false).to(true)
+ end
+ end
+
+ describe "#down" do
+ it 'does nothing' do
+ record = settings.create!(elasticsearch_requeue_workers: true)
+
+ migrate!
+
+ expect { schema_migrate_down! }.not_to change { record.reload.elasticsearch_requeue_workers }
+ end
+
+ it 'updates elasticsearch_requeue_workers when gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ record = settings.create!(elasticsearch_requeue_workers: true)
+
+ migrate!
+
+ expect { schema_migrate_down! }.to change { record.reload.elasticsearch_requeue_workers }.from(true).to(false)
+ end
+ end
+end
diff --git a/spec/migrations/20230621074611_update_elasticsearch_number_of_shards_in_application_settings_for_gitlab_com_spec.rb b/spec/migrations/20230621074611_update_elasticsearch_number_of_shards_in_application_settings_for_gitlab_com_spec.rb
new file mode 100644
index 00000000000..80a1f9f59e2
--- /dev/null
+++ b/spec/migrations/20230621074611_update_elasticsearch_number_of_shards_in_application_settings_for_gitlab_com_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateElasticsearchNumberOfShardsInApplicationSettingsForGitlabCom, feature_category: :global_search do
+ let(:settings) { table(:application_settings) }
+
+ describe "#up" do
+ it 'does nothing when not in gitlab.com' do
+ record = settings.create!
+
+ expect { migrate! }.not_to change { record.reload.elasticsearch_worker_number_of_shards }
+ end
+
+ it 'updates elasticsearch_worker_number_of_shards when gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ record = settings.create!
+
+ expect { migrate! }.to change { record.reload.elasticsearch_worker_number_of_shards }.from(2).to(16)
+ end
+ end
+
+ describe "#down" do
+ it 'does nothing when not in gitlab.com' do
+ record = settings.create!(elasticsearch_worker_number_of_shards: 16)
+
+ migrate!
+
+ expect { schema_migrate_down! }.not_to change { record.reload.elasticsearch_worker_number_of_shards }
+ end
+
+ it 'updates elasticsearch_worker_number_of_shards when gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ record = settings.create!(elasticsearch_worker_number_of_shards: 16)
+
+ migrate!
+
+ expect { schema_migrate_down! }.to change { record.reload.elasticsearch_worker_number_of_shards }.from(16).to(2)
+ end
+ end
+end
diff --git a/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb b/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb
new file mode 100644
index 00000000000..f6c470260ff
--- /dev/null
+++ b/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillMissingCiCdSettings, feature_category: :source_code_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: :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/20230629095819_queue_backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb b/spec/migrations/20230629095819_queue_backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb
new file mode 100644
index 00000000000..eb9a131008a
--- /dev/null
+++ b/spec/migrations/20230629095819_queue_backfill_uuid_conversion_column_in_vulnerability_occurrences_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillUuidConversionColumnInVulnerabilityOccurrences, 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_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
+end
diff --git a/spec/migrations/20230703024031_cleanup_project_pipeline_status_key_spec.rb b/spec/migrations/20230703024031_cleanup_project_pipeline_status_key_spec.rb
new file mode 100644
index 00000000000..4232162134a
--- /dev/null
+++ b/spec/migrations/20230703024031_cleanup_project_pipeline_status_key_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanupProjectPipelineStatusKey, feature_category: :redis do
+ it 'enqueues a RedisMigrationWorker job from cursor 0' do
+ expect(RedisMigrationWorker).to receive(:perform_async).with('BackfillProjectPipelineStatusTtl', '0')
+
+ migrate!
+ end
+end
diff --git a/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb b/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb
new file mode 100644
index 00000000000..dc4adae9138
--- /dev/null
+++ b/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanupBigintConversionForMergeRequestMetricsForSelfHosts, feature_category: :database do
+ after do
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE merge_request_metrics DROP COLUMN IF EXISTS id_convert_to_bigint')
+ end
+
+ describe '#up' do
+ context 'when is GitLab.com, dev, or test' 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 DROP COLUMN IF EXISTS id_convert_to_bigint')
+ end
+
+ it 'does nothing' 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_convert_to_bigint' }).to be nil
+ }
+
+ migration.after -> {
+ merge_request_metrics.reset_column_information
+
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
+ }
+ end
+ end
+ end
+ end
+
+ context 'when is a self-host customer with the temporary column already dropped' 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 bigint')
+ connection.execute('ALTER TABLE merge_request_metrics DROP COLUMN IF EXISTS id_convert_to_bigint')
+ end
+
+ it 'does nothing' 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)
+
+ migrate!
+
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
+ end
+ end
+
+ context 'when is a self-host with the temporary columns' 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 bigint')
+ connection.execute('ALTER TABLE merge_request_metrics ADD COLUMN IF NOT EXISTS id_convert_to_bigint integer')
+ end
+
+ it 'drop the temporary columns' 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('bigint')
+ expect(merge_request_metrics.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('integer')
+ }
+
+ 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 { |c| c.name == 'id_convert_to_bigint' }).to be nil
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/deduplicate_inactive_alert_integrations_spec.rb b/spec/migrations/deduplicate_inactive_alert_integrations_spec.rb
new file mode 100644
index 00000000000..7f963a9bd0a
--- /dev/null
+++ b/spec/migrations/deduplicate_inactive_alert_integrations_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe DeduplicateInactiveAlertIntegrations, feature_category: :incident_management do
+ let!(:namespace_class) { table(:namespaces) }
+ let!(:project_class) { table(:projects) }
+ let!(:integration_class) { table(:alert_management_http_integrations) }
+
+ let!(:namespace_0) { namespace_class.create!(name: 'namespace1', path: 'namespace1') }
+ let!(:namespace_1) { namespace_class.create!(name: 'namespace2', path: 'namespace2') }
+ let!(:namespace_2) { namespace_class.create!(name: 'namespace3', path: 'namespace3') }
+
+ let!(:project_with_inactive_duplicate) { create_project(namespace_0, namespace_0) }
+ let!(:project_with_multiple_duplicates) { create_project(namespace_0, namespace_1) }
+ let!(:project_without_duplicates) { create_project(namespace_0, namespace_2) }
+
+ let!(:integrations) do
+ [
+ create_integration(project_with_inactive_duplicate, 'default'),
+ create_integration(project_with_inactive_duplicate, 'other'),
+ create_integration(project_with_inactive_duplicate, 'other', active: false),
+ create_integration(project_with_multiple_duplicates, 'default', active: false),
+ create_integration(project_with_multiple_duplicates, 'default', active: false),
+ create_integration(project_with_multiple_duplicates, 'other', active: false),
+ create_integration(project_with_multiple_duplicates, 'other'),
+ create_integration(project_without_duplicates, 'default'),
+ create_integration(project_without_duplicates, 'other', active: false)
+ ]
+ end
+
+ describe '#up' do
+ it 'updates the endpoint identifier of duplicate inactive integrations' do
+ expect { migrate! }
+ .to not_change { integrations[0].reload }
+ .and not_change { integrations[1].reload }
+ .and not_change { integrations[6].reload }
+ .and not_change { integrations[7].reload }
+ .and not_change { integrations[8].reload }
+
+ expect { integrations[2].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { integrations[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { integrations[4].reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { integrations[5].reload }.to raise_error(ActiveRecord::RecordNotFound)
+
+ endpoints = integration_class.pluck(:endpoint_identifier, :project_id)
+ expect(endpoints.uniq).to match_array(endpoints)
+ end
+ end
+
+ private
+
+ def create_integration(project, endpoint_identifier, active: true)
+ integration_class.create!(
+ project_id: project.id,
+ endpoint_identifier: endpoint_identifier,
+ active: active,
+ encrypted_token_iv: 'iv',
+ encrypted_token: 'token',
+ name: "HTTP Integration - #{endpoint_identifier}"
+ )
+ end
+
+ def create_project(namespace, project_namespace)
+ project_class.create!(
+ namespace_id: namespace.id,
+ project_namespace_id: project_namespace.id
+ )
+ end
+end
diff --git a/spec/migrations/ensure_events_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb b/spec/migrations/ensure_events_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..6694b690d30
--- /dev/null
+++ b/spec/migrations/ensure_events_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EnsureEventsBigintBackfillIsFinishedForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ let(:migration_arguments) do
+ {
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: 'events',
+ column_name: 'id',
+ job_arguments: [['target_id'], ['target_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/models/abuse/trust_score_spec.rb b/spec/models/abuse/trust_score_spec.rb
index 755309ac699..85fffcc167b 100644
--- a/spec/models/abuse/trust_score_spec.rb
+++ b/spec/models/abuse/trust_score_spec.rb
@@ -26,7 +26,6 @@ RSpec.describe Abuse::TrustScore, feature_category: :instance_resiliency do
before do
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('123abc')
- stub_const('Abuse::TrustScore::MAX_EVENTS', 2)
end
context 'if correlation ID is nil' do
@@ -42,16 +41,5 @@ RSpec.describe Abuse::TrustScore, feature_category: :instance_resiliency do
expect(subject.correlation_id_value).to eq('already-set')
end
end
-
- context 'if max events is exceeded' do
- it 'removes the oldest events' do
- first = create(:abuse_trust_score, user: user)
- create(:abuse_trust_score, user: user)
- create(:abuse_trust_score, user: user)
-
- expect(user.abuse_trust_scores.count).to eq(2)
- expect(described_class.find_by_id(first.id)).to eq(nil)
- end
- end
end
end
diff --git a/spec/models/abuse/user_trust_score_spec.rb b/spec/models/abuse/user_trust_score_spec.rb
new file mode 100644
index 00000000000..6b2cfa6a504
--- /dev/null
+++ b/spec/models/abuse/user_trust_score_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Abuse::UserTrustScore, feature_category: :instance_resiliency do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let(:user_1_scores) { described_class.new(user1) }
+ let(:user_2_scores) { described_class.new(user2) }
+
+ describe '#spammer?' do
+ context 'when the user is a spammer' do
+ before do
+ allow(user_1_scores).to receive(:spam_score).and_return(0.9)
+ end
+
+ it 'classifies the user as a spammer' do
+ expect(user_1_scores).to be_spammer
+ end
+ end
+
+ context 'when the user is not a spammer' do
+ before do
+ allow(user_1_scores).to receive(:spam_score).and_return(0.1)
+ end
+
+ it 'does not classify the user as a spammer' do
+ expect(user_1_scores).not_to be_spammer
+ end
+ end
+ end
+
+ describe '#spam_score' do
+ context 'when the user is a spammer' do
+ before do
+ create(:abuse_trust_score, user: user1, score: 0.8)
+ create(:abuse_trust_score, user: user1, score: 0.9)
+ end
+
+ it 'returns the expected score' do
+ expect(user_1_scores.spam_score).to be_within(0.01).of(0.85)
+ end
+ end
+
+ context 'when the user is not a spammer' do
+ before do
+ create(:abuse_trust_score, user: user1, score: 0.1)
+ create(:abuse_trust_score, user: user1, score: 0.0)
+ end
+
+ it 'returns the expected score' do
+ expect(user_1_scores.spam_score).to be_within(0.01).of(0.05)
+ end
+ end
+ end
+
+ describe '#telesign_score' do
+ context 'when the user has a telesign risk score' do
+ before do
+ create(:abuse_trust_score, user: user1, score: 12.0, source: :telesign)
+ create(:abuse_trust_score, user: user1, score: 24.0, source: :telesign)
+ end
+
+ it 'returns the latest score' do
+ expect(user_1_scores.telesign_score).to be(24.0)
+ end
+ end
+
+ context 'when the user does not have a telesign risk score' do
+ it 'defaults to zero' do
+ expect(user_2_scores.telesign_score).to be(0.0)
+ end
+ end
+ end
+
+ describe '#arkose_global_score' do
+ context 'when the user has an arkose global risk score' do
+ before do
+ create(:abuse_trust_score, user: user1, score: 12.0, source: :arkose_global_score)
+ create(:abuse_trust_score, user: user1, score: 24.0, source: :arkose_global_score)
+ end
+
+ it 'returns the latest score' do
+ expect(user_1_scores.arkose_global_score).to be(24.0)
+ end
+ end
+
+ context 'when the user does not have an arkose global risk score' do
+ it 'defaults to zero' do
+ expect(user_2_scores.arkose_global_score).to be(0.0)
+ end
+ end
+ end
+
+ describe '#arkose_custom_score' do
+ context 'when the user has an arkose custom risk score' do
+ before do
+ create(:abuse_trust_score, user: user1, score: 12.0, source: :arkose_custom_score)
+ create(:abuse_trust_score, user: user1, score: 24.0, source: :arkose_custom_score)
+ end
+
+ it 'returns the latest score' do
+ expect(user_1_scores.arkose_custom_score).to be(24.0)
+ end
+ end
+
+ context 'when the user does not have an arkose custom risk score' do
+ it 'defaults to zero' do
+ expect(user_2_scores.arkose_custom_score).to be(0.0)
+ end
+ end
+ end
+
+ describe '#remove_old_scores' do
+ context 'if max events is exceeded' do
+ before do
+ stub_const('Abuse::UserTrustScore::MAX_EVENTS', 2)
+ end
+
+ it 'removes the oldest events' do
+ first = create(:abuse_trust_score, user: user1)
+ create(:abuse_trust_score, user: user1)
+ create(:abuse_trust_score, user: user1)
+
+ expect(user1.abuse_trust_scores.count).to eq(2)
+ expect(Abuse::TrustScore.find_by_id(first.id)).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index 8717b2a1075..54169c254a6 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -24,19 +24,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
describe '#current?' do
it 'returns true if the active session matches the current session' do
- active_session = ActiveSession.new(session_private_id: rack_session.private_id)
+ active_session = described_class.new(session_private_id: rack_session.private_id)
expect(active_session.current?(session)).to be true
end
it 'returns false if the active session does not match the current session' do
- active_session = ActiveSession.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d'))
+ active_session = described_class.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d'))
expect(active_session.current?(session)).to be false
end
it 'returns false if the session id is nil' do
- active_session = ActiveSession.new(session_id: nil)
+ active_session = described_class.new(session_id: nil)
session = double(:session, id: nil)
expect(active_session.current?(session)).to be false
@@ -96,7 +96,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
)
end
- expect(ActiveSession.list(user)).to contain_exactly(session)
+ expect(described_class.list(user)).to contain_exactly(session)
Gitlab::Redis::Sessions.with do |redis|
expect(redis.sscan_each(lookup_key)).to contain_exactly session_id
@@ -119,7 +119,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
it 'returns an empty array if the user does not have any active session' do
- expect(ActiveSession.list(user)).to be_empty
+ expect(described_class.list(user)).to be_empty
end
end
@@ -138,7 +138,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
)
end
- expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
+ expect(described_class.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
end
end
@@ -148,7 +148,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
redis.sadd(lookup_key, %w[a b c])
end
- expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to match_array(%w[a b c])
+ expect(described_class.session_ids_for_user(user.id).map(&:to_s)).to match_array(%w[a b c])
end
end
@@ -159,13 +159,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
redis.set("session:gitlab:#{rack_session.private_id}", Marshal.dump({ _csrf_token: 'abcd' }))
end
- expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
+ expect(described_class.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
end
it 'avoids a redis lookup for an empty array' do
expect(Gitlab::Redis::Sessions).not_to receive(:with)
- expect(ActiveSession.sessions_from_ids([])).to eq([])
+ expect(described_class.sessions_from_ids([])).to eq([])
end
it 'uses redis lookup in batches' do
@@ -178,13 +178,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
mget_responses = sessions.map { |session| [Marshal.dump(session)] }
expect(redis).to receive(:mget).twice.times.and_return(*mget_responses)
- expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions)
+ expect(described_class.sessions_from_ids([1, 2])).to eql(sessions)
end
end
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
- ActiveSession.set(user, request)
+ described_class.set(user, request)
session_id = "2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae"
@@ -276,10 +276,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
context 'destroy called with Rack::Session::SessionId#private_id' do
- subject { ActiveSession.destroy_session(user, rack_session.private_id) }
+ subject { described_class.destroy_session(user, rack_session.private_id) }
it 'calls .destroy_sessions' do
- expect(ActiveSession).to(
+ expect(described_class).to(
receive(:destroy_sessions)
.with(anything, user, [rack_session.private_id]))
@@ -287,7 +287,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
context 'ActiveSession with session_private_id' do
- let(:active_session) { ActiveSession.new(session_private_id: rack_session.private_id) }
+ let(:active_session) { described_class.new(session_private_id: rack_session.private_id) }
let(:active_session_lookup_key) { rack_session.private_id }
context 'when using old session key serialization' do
@@ -311,7 +311,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it 'gracefully handles a nil session ID' do
expect(described_class).not_to receive(:destroy_sessions)
- ActiveSession.destroy_all_but_current(user, nil)
+ described_class.destroy_all_but_current(user, nil)
end
shared_examples 'with user sessions' do
@@ -338,15 +338,15 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
it 'removes the entry associated with the all user sessions but current' do
- expect { ActiveSession.destroy_all_but_current(user, request.session) }
+ expect { described_class.destroy_all_but_current(user, request.session) }
.to(change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1))
- expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
+ expect(described_class.session_ids_for_user(9999).size).to eq(1)
end
it 'removes the lookup entry of deleted sessions' do
session_private_id = Rack::Session::SessionId.new(current_session_id).private_id
- ActiveSession.destroy_all_but_current(user, request.session)
+ described_class.destroy_all_but_current(user, request.session)
Gitlab::Redis::Sessions.with do |redis|
expect(redis.smembers(lookup_key)).to contain_exactly session_private_id
@@ -361,9 +361,9 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
redis.sadd?(lookup_key, impersonated_session_id)
end
- expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2)
+ expect { described_class.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2)
- expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
+ expect(described_class.session_ids_for_user(9999).size).to eq(1)
end
end
@@ -407,7 +407,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
redis.sadd(lookup_key, [current_session_id, '59822c7d9fcdfa03725eff41782ad97d'])
end
- ActiveSession.cleanup(user)
+ described_class.cleanup(user)
Gitlab::Redis::Sessions.with do |redis|
expect(redis.smembers(lookup_key)).to contain_exactly current_session_id
@@ -416,7 +416,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
it 'does not bail if there are no lookup entries' do
- ActiveSession.cleanup(user)
+ described_class.cleanup(user)
end
context 'cleaning up old sessions' do
@@ -436,7 +436,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
it 'removes obsolete active sessions entries' do
- ActiveSession.cleanup(user)
+ described_class.cleanup(user)
Gitlab::Redis::Sessions.with do |redis|
sessions = described_class.list(user)
@@ -450,7 +450,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end
it 'removes obsolete lookup entries' do
- ActiveSession.cleanup(user)
+ described_class.cleanup(user)
Gitlab::Redis::Sessions.with do |redis|
lookup_entries = redis.smembers(lookup_key)
@@ -465,7 +465,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
redis.sadd?(lookup_key, (max_number_of_sessions_plus_two + 1).to_s)
end
- ActiveSession.cleanup(user)
+ described_class.cleanup(user)
Gitlab::Redis::Sessions.with do |redis|
lookup_entries = redis.smembers(lookup_key)
@@ -654,7 +654,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
let(:auth) { double(cookies: {}) }
it 'sets marketing cookie' do
- ActiveSession.set_active_user_cookie(auth)
+ described_class.set_active_user_cookie(auth)
expect(auth.cookies[:about_gitlab_active_user][:value]).to be_truthy
end
end
@@ -663,11 +663,11 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
let(:auth) { double(cookies: {}) }
before do
- ActiveSession.set_active_user_cookie(auth)
+ described_class.set_active_user_cookie(auth)
end
it 'unsets marketing cookie' do
- ActiveSession.unset_active_user_cookie(auth)
+ described_class.unset_active_user_cookie(auth)
expect(auth.cookies[:about_gitlab_active_user]).to be_nil
end
end
diff --git a/spec/models/ai/service_access_token_spec.rb b/spec/models/ai/service_access_token_spec.rb
new file mode 100644
index 00000000000..12ed24f3bd6
--- /dev/null
+++ b/spec/models/ai/service_access_token_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ai::ServiceAccessToken, type: :model, feature_category: :application_performance do
+ describe '.expired', :freeze_time do
+ let_it_be(:expired_token) { create(:service_access_token, :code_suggestions, :expired) }
+ let_it_be(:active_token) { create(:service_access_token, :code_suggestions, :active) }
+
+ it 'selects all expired tokens' do
+ expect(described_class.expired).to match_array([expired_token])
+ end
+ end
+
+ # There is currently only one category, please expand this test when a new category is added.
+ describe '.for_category' do
+ let(:code_suggestions_token) { create(:service_access_token, :code_suggestions) }
+ let(:category) { :code_suggestions }
+
+ it 'only selects tokens from the selected category' do
+ expect(described_class.for_category(category)).to match_array([code_suggestions_token])
+ end
+ end
+
+ describe '#token' do
+ let(:token_value) { 'Abc' }
+
+ it 'is encrypted' do
+ subject.token = token_value
+
+ aggregate_failures do
+ expect(subject.encrypted_token_iv).to be_present
+ expect(subject.encrypted_token).to be_present
+ expect(subject.encrypted_token).not_to eq(token_value)
+ expect(subject.token).to eq(token_value)
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:token) }
+ it { is_expected.to validate_presence_of(:category) }
+ it { is_expected.to validate_presence_of(:expires_at) }
+ end
+ end
+end
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index ff77ca2ab64..ed5cde3ca3b 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe AlertManagement::Alert do
+RSpec.describe AlertManagement::Alert, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:project2) { create(:project) }
- let_it_be(:triggered_alert, reload: true) { create(:alert_management_alert, :triggered, project: project) }
- let_it_be(:acknowledged_alert, reload: true) { create(:alert_management_alert, :acknowledged, project: project) }
- let_it_be(:resolved_alert, reload: true) { create(:alert_management_alert, :resolved, project: project2) }
- let_it_be(:ignored_alert, reload: true) { create(:alert_management_alert, :ignored, project: project2) }
+ let_it_be_with_refind(:triggered_alert) { create(:alert_management_alert, :triggered, project: project) }
+ let_it_be_with_refind(:acknowledged_alert) { create(:alert_management_alert, :acknowledged, project: project) }
+ let_it_be_with_refind(:resolved_alert) { create(:alert_management_alert, :resolved, project: project2) }
+ let_it_be_with_refind(:ignored_alert) { create(:alert_management_alert, :ignored, project: project2) }
describe 'associations' do
it { is_expected.to belong_to(:project) }
@@ -168,7 +168,7 @@ RSpec.describe AlertManagement::Alert do
let_it_be(:alert) { triggered_alert }
let_it_be(:assignee) { create(:user) }
- subject { AlertManagement::Alert.for_assignee_username(assignee_username) }
+ subject { described_class.for_assignee_username(assignee_username) }
before_all do
alert.update!(assignees: [assignee])
@@ -269,7 +269,7 @@ RSpec.describe AlertManagement::Alert do
alert.update!(title: 'Title', description: 'Desc', service: 'Service', monitoring_tool: 'Monitor')
end
- subject { AlertManagement::Alert.search(query) }
+ subject { described_class.search(query) }
context 'does not contain search string' do
let(:query) { 'something else' }
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb
index 606b53aeacd..479ae8a4966 100644
--- a/spec/models/alert_management/http_integration_spec.rb
+++ b/spec/models/alert_management/http_integration_spec.rb
@@ -29,13 +29,13 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
# Uniqueness spec saves integration with `validate: false` otherwise.
subject { create(:alert_management_http_integration, :legacy) }
- it { is_expected.to validate_uniqueness_of(:endpoint_identifier).scoped_to(:project_id, :active) }
+ it { is_expected.to validate_uniqueness_of(:endpoint_identifier).scoped_to(:project_id) }
end
context 'when inactive' do
subject { create(:alert_management_http_integration, :legacy, :inactive) }
- it { is_expected.not_to validate_uniqueness_of(:endpoint_identifier).scoped_to(:project_id, :active) }
+ it { is_expected.to validate_uniqueness_of(:endpoint_identifier).scoped_to(:project_id) }
end
context 'payload_attribute_mapping' do
@@ -90,7 +90,7 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
describe 'scopes' do
let_it_be(:integration_1) { create(:alert_management_http_integration) }
let_it_be(:integration_2) { create(:alert_management_http_integration, :inactive, project: project) }
- let_it_be(:integration_3) { create(:alert_management_http_integration, :prometheus, project: project) }
+ let_it_be(:integration_3) { create(:alert_management_prometheus_integration, project: project) }
let_it_be(:integration_4) { create(:alert_management_http_integration, :legacy, :inactive) }
describe '.for_endpoint_identifier' do
@@ -129,12 +129,6 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
it { is_expected.to contain_exactly(integration_1, integration_3) }
end
- describe '.legacy' do
- subject { described_class.legacy }
-
- it { is_expected.to contain_exactly(integration_4) }
- end
-
describe '.ordered_by_type_and_id' do
before do
# Rearrange cache by saving to avoid false-positives
@@ -232,9 +226,16 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
end
context 'when included in initialization args' do
- let(:integration) { described_class.new(endpoint_identifier: 'legacy') }
+ let(:required_args) { { project: project, name: 'Name' } }
- it { is_expected.to eq('legacy') }
+ AlertManagement::HttpIntegration::LEGACY_IDENTIFIERS.each do |identifier|
+ context "for endpoint identifier \"#{identifier}\"" do
+ let(:integration) { described_class.new(**required_args, endpoint_identifier: identifier) }
+
+ it { is_expected.to eq(identifier) }
+ specify { expect(integration).to be_valid }
+ end
+ end
end
context 'when reassigning' do
@@ -293,7 +294,7 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
end
context 'for a prometheus integration' do
- let(:integration) { build(:alert_management_http_integration, :prometheus) }
+ let(:integration) { build(:alert_management_prometheus_integration) }
it do
is_expected.to eq(
@@ -307,7 +308,7 @@ RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_man
end
context 'for a legacy integration' do
- let(:integration) { build(:alert_management_http_integration, :prometheus, :legacy) }
+ let(:integration) { build(:alert_management_prometheus_integration, :legacy) }
it do
is_expected.to eq(
diff --git a/spec/models/analytics/cycle_analytics/stage_spec.rb b/spec/models/analytics/cycle_analytics/stage_spec.rb
index 960d8d3e964..54ae0feca2c 100644
--- a/spec/models/analytics/cycle_analytics/stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/stage_spec.rb
@@ -3,10 +3,23 @@
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream_management do
- describe 'uniqueness validation on name' do
+ describe 'validations' do
subject { build(:cycle_analytics_stage) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to([:group_id, :group_value_stream_id]) }
+
+ it 'validates count of stages per value stream' do
+ stub_const("#{described_class.name}::MAX_STAGES_PER_VALUE_STREAM", 1)
+ value_stream = create(:cycle_analytics_value_stream, name: 'test')
+ create(:cycle_analytics_stage, name: "stage 1", value_stream: value_stream)
+
+ new_stage = build(:cycle_analytics_stage, name: "stage 2", value_stream: value_stream)
+
+ expect do
+ new_stage.save!
+ end.to raise_error(ActiveRecord::RecordInvalid,
+ _('Validation failed: Value stream Maximum number of stages per value stream exceeded'))
+ end
end
describe 'associations' do
diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
index f290cf25ae6..3b3187e0b51 100644
--- a/spec/models/analytics/cycle_analytics/value_stream_spec.rb
+++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
@@ -25,6 +25,19 @@ RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_cat
it_behaves_like 'value stream analytics namespace models' do
let(:factory_name) { :cycle_analytics_value_stream }
end
+
+ it 'validates count of value streams per namespace' do
+ stub_const("#{described_class.name}::MAX_VALUE_STREAMS_PER_NAMESPACE", 1)
+ group = create(:group)
+ create(:cycle_analytics_value_stream, name: 'test', namespace: group)
+
+ new_value_stream = build(:cycle_analytics_value_stream, name: 'test2', namespace: group)
+
+ expect do
+ new_value_stream.save!
+ end.to raise_error(ActiveRecord::RecordInvalid,
+ _('Validation failed: Namespace Maximum number of value streams per namespace exceeded'))
+ end
end
describe 'scopes' do
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index ee3065cf8f2..9ea5c1ec92c 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -257,7 +257,7 @@ RSpec.describe ApplicationRecord do
end
before do
- ApplicationRecord.connection.execute(<<~SQL)
+ described_class.connection.execute(<<~SQL)
create table _test_tests (
id bigserial primary key not null,
ignore_me text
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 12ab061fa03..8dcafaa90a0 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -59,8 +59,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
- it { is_expected.to allow_value(true).for(:container_expiration_policies_enable_historic_entries) }
- it { is_expected.to allow_value(false).for(:container_expiration_policies_enable_historic_entries) }
+ it { is_expected.to allow_value(true, false).for(:container_expiration_policies_enable_historic_entries) }
it { is_expected.not_to allow_value(nil).for(:container_expiration_policies_enable_historic_entries) }
it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
@@ -100,8 +99,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_numericality_of(:container_registry_cleanup_tags_service_max_list_size).only_integer.is_greater_than_or_equal_to(0) }
it { is_expected.to validate_numericality_of(:container_registry_data_repair_detail_worker_max_concurrency).only_integer.is_greater_than_or_equal_to(0) }
it { is_expected.to validate_numericality_of(:container_registry_expiration_policies_worker_capacity).only_integer.is_greater_than_or_equal_to(0) }
- it { is_expected.to allow_value(true).for(:container_registry_expiration_policies_caching) }
- it { is_expected.to allow_value(false).for(:container_registry_expiration_policies_caching) }
+ it { is_expected.to allow_value(true, false).for(:container_registry_expiration_policies_caching) }
it { is_expected.to validate_numericality_of(:container_registry_import_max_tags_count).only_integer.is_greater_than_or_equal_to(0) }
it { is_expected.to validate_numericality_of(:container_registry_import_max_retries).only_integer.is_greater_than_or_equal_to(0) }
@@ -134,8 +132,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
- it { is_expected.to allow_value(true).for(:wiki_asciidoc_allow_uri_includes) }
- it { is_expected.to allow_value(false).for(:wiki_asciidoc_allow_uri_includes) }
+ it { is_expected.to allow_value(true, false).for(:wiki_asciidoc_allow_uri_includes) }
it { is_expected.not_to allow_value(nil).for(:wiki_asciidoc_allow_uri_includes) }
it { is_expected.to validate_presence_of(:max_artifacts_size) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
@@ -148,8 +145,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_presence_of(:max_terraform_state_size_bytes) }
it { is_expected.to validate_numericality_of(:max_terraform_state_size_bytes).only_integer.is_greater_than_or_equal_to(0) }
- it { is_expected.to allow_value(true).for(:user_defaults_to_private_profile) }
- it { is_expected.to allow_value(false).for(:user_defaults_to_private_profile) }
+ it { is_expected.to allow_value(true, 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) }
@@ -250,16 +246,13 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to allow_value(http).for(:jira_connect_proxy_url) }
it { is_expected.to allow_value(https).for(:jira_connect_proxy_url) }
- it { is_expected.to allow_value(true).for(:bulk_import_enabled) }
- it { is_expected.to allow_value(false).for(:bulk_import_enabled) }
+ it { is_expected.to allow_value(true, false).for(:bulk_import_enabled) }
it { is_expected.not_to allow_value(nil).for(:bulk_import_enabled) }
- it { is_expected.to allow_value(true).for(:allow_runner_registration_token) }
- it { is_expected.to allow_value(false).for(:allow_runner_registration_token) }
+ it { is_expected.to allow_value(true, 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.to allow_value(true, false).for(:gitlab_dedicated_instance) }
it { is_expected.not_to allow_value(nil).for(:gitlab_dedicated_instance) }
it { is_expected.not_to allow_value(random: :value).for(:database_apdex_settings) }
@@ -493,6 +486,37 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
end
+ describe 'GitLab for Slack app settings' do
+ before do
+ setting.slack_app_enabled = slack_app_enabled
+ end
+
+ context 'when GitLab for Slack app is disabled' do
+ let(:slack_app_enabled) { false }
+
+ it { is_expected.to allow_value(nil).for(:slack_app_id) }
+ it { is_expected.to allow_value(nil).for(:slack_app_secret) }
+ it { is_expected.to allow_value(nil).for(:slack_app_signing_secret) }
+ it { is_expected.to allow_value(nil).for(:slack_app_verification_token) }
+ end
+
+ context 'when GitLab for Slack app is enabled' do
+ let(:slack_app_enabled) { true }
+
+ it { is_expected.to allow_value('123456789a').for(:slack_app_id) }
+ it { is_expected.not_to allow_value(nil).for(:slack_app_id) }
+
+ it { is_expected.to allow_value('secret').for(:slack_app_secret) }
+ it { is_expected.not_to allow_value(nil).for(:slack_app_secret) }
+
+ it { is_expected.to allow_value('signing-secret').for(:slack_app_signing_secret) }
+ it { is_expected.not_to allow_value(nil).for(:slack_app_signing_secret) }
+
+ it { is_expected.to allow_value('token').for(:slack_app_verification_token) }
+ it { is_expected.not_to allow_value(nil).for(:slack_app_verification_token) }
+ end
+ end
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
expect do
@@ -1569,7 +1593,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it 'ignores the plaintext token' do
subject
- ApplicationSetting.update_all(static_objects_external_storage_auth_token: 'Test')
+ described_class.update_all(static_objects_external_storage_auth_token: 'Test')
setting.reload
expect(setting[:static_objects_external_storage_auth_token]).to be_nil
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 99006f8ddce..586ec8f723a 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -141,36 +141,51 @@ RSpec.describe AwardEmoji do
end
end
- describe 'expiring ETag cache' do
+ describe 'broadcasting updates' do
context 'on a note' do
let(:note) { create(:note_on_issue) }
let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: note) }
- it 'calls expire_etag_cache on the note when saved' do
+ it 'broadcasts updates on the note when saved' do
expect(note).to receive(:expire_etag_cache)
+ expect(note).to receive(:trigger_note_subscription_update)
award_emoji.save!
end
- it 'calls expire_etag_cache on the note when destroyed' do
+ it 'broadcasts updates on the note when destroyed' do
expect(note).to receive(:expire_etag_cache)
+ expect(note).to receive(:trigger_note_subscription_update)
award_emoji.destroy!
end
+
+ context 'when importing' do
+ let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: note, importing: true) }
+
+ it 'does not broadcast updates on the note when saved' do
+ expect(note).not_to receive(:expire_etag_cache)
+ expect(note).not_to receive(:trigger_note_subscription_update)
+
+ award_emoji.save!
+ end
+ end
end
context 'on another awardable' do
let(:issue) { create(:issue) }
let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: issue) }
- it 'does not call expire_etag_cache on the issue when saved' do
+ it 'does not broadcast updates on the issue when saved' do
expect(issue).not_to receive(:expire_etag_cache)
+ expect(issue).not_to receive(:trigger_note_subscription_update)
award_emoji.save!
end
- it 'does not call expire_etag_cache on the issue when destroyed' do
+ it 'does not broadcast updates on the issue when destroyed' do
expect(issue).not_to receive(:expire_etag_cache)
+ expect(issue).not_to receive(:trigger_note_subscription_update)
award_emoji.destroy!
end
diff --git a/spec/models/bulk_import_spec.rb b/spec/models/bulk_import_spec.rb
index acb1f4a2ef7..a50fc6eaba4 100644
--- a/spec/models/bulk_import_spec.rb
+++ b/spec/models/bulk_import_spec.rb
@@ -75,4 +75,22 @@ RSpec.describe BulkImport, type: :model, feature_category: :importers do
end
end
end
+
+ describe '#supports_batched_export?' do
+ context 'when source version is greater than min supported version for batched migrations' do
+ it 'returns true' do
+ bulk_import = build(:bulk_import, source_version: '16.2.0')
+
+ expect(bulk_import.supports_batched_export?).to eq(true)
+ end
+ end
+
+ context 'when source version is less than min supported version for batched migrations' do
+ it 'returns false' do
+ bulk_import = build(:bulk_import, source_version: '15.5.0')
+
+ expect(bulk_import.supports_batched_export?).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index c7ace3d2b78..7179ed7cb42 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -255,6 +255,27 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
expect(entity.export_relations_url_path).to eq("/projects/#{entity.source_xid}/export_relations")
end
end
+
+ context 'when batched' do
+ context 'when source supports batched export' do
+ it 'returns batched export relations url' do
+ import = build(:bulk_import, source_version: '16.2.0')
+ entity = build(:bulk_import_entity, :project_entity, bulk_import: import)
+
+ expect(entity.export_relations_url_path(batched: true))
+ .to eq("/projects/#{entity.source_xid}/export_relations?batched=true")
+ end
+ end
+
+ context 'when source does not support batched export' do
+ it 'returns export relations url' do
+ entity = build(:bulk_import_entity)
+
+ expect(entity.export_relations_url_path(batched: true))
+ .to eq("/groups/#{entity.source_xid}/export_relations")
+ end
+ end
+ end
end
describe '#relation_download_url_path' do
@@ -264,6 +285,27 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
expect(entity.relation_download_url_path('test'))
.to eq("/groups/#{entity.source_xid}/export_relations/download?relation=test")
end
+
+ context 'when batch number is present' do
+ context 'when source supports batched export' do
+ it 'returns export relations url with download query string and batch number' do
+ import = build(:bulk_import, source_version: '16.2.0')
+ entity = build(:bulk_import_entity, :project_entity, bulk_import: import)
+
+ expect(entity.relation_download_url_path('test', 1))
+ .to eq("/projects/#{entity.source_xid}/export_relations/download?batch_number=1&batched=true&relation=test")
+ end
+ end
+
+ context 'when source does not support batched export' do
+ it 'returns export relations url' do
+ entity = build(:bulk_import_entity)
+
+ expect(entity.relation_download_url_path('test', 1))
+ .to eq("/groups/#{entity.source_xid}/export_relations/download?relation=test")
+ end
+ end
+ end
end
describe '#entity_type' do
diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb
index 7173d032bc2..e5c0632b113 100644
--- a/spec/models/bulk_imports/export_spec.rb
+++ b/spec/models/bulk_imports/export_spec.rb
@@ -83,4 +83,29 @@ RSpec.describe BulkImports::Export, type: :model, feature_category: :importers d
end
end
end
+
+ describe '#remove_existing_upload!' do
+ context 'when upload exists' do
+ it 'removes the upload' do
+ export = create(:bulk_import_export)
+ upload = create(:bulk_import_export_upload, export: export)
+ upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
+
+ expect_any_instance_of(BulkImports::ExportUpload) do |upload|
+ expect(upload).to receive(:remove_export_file!)
+ expect(upload).to receive(:save!)
+ end
+
+ export.remove_existing_upload!
+ end
+ end
+
+ context 'when upload does not exist' do
+ it 'returns' do
+ export = build(:bulk_import_export)
+
+ expect { export.remove_existing_upload! }.not_to change { export.upload }
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/export_status_spec.rb b/spec/models/bulk_imports/export_status_spec.rb
index 0921c3bdce2..c3faa2db19c 100644
--- a/spec/models/bulk_imports/export_status_spec.rb
+++ b/spec/models/bulk_imports/export_status_spec.rb
@@ -2,16 +2,28 @@
require 'spec_helper'
-RSpec.describe BulkImports::ExportStatus do
+RSpec.describe BulkImports::ExportStatus, feature_category: :importers do
let_it_be(:relation) { 'labels' }
let_it_be(:import) { create(:bulk_import) }
let_it_be(:config) { create(:bulk_import_configuration, bulk_import: import) }
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: import, source_full_path: 'foo') }
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:batched) { false }
+ let(:batches) { [] }
let(:response_double) do
instance_double(HTTParty::Response,
- parsed_response: [{ 'relation' => 'labels', 'status' => status, 'error' => 'error!' }]
+ parsed_response: [
+ {
+ 'relation' => 'labels',
+ 'status' => status,
+ 'error' => 'error!',
+ 'batched' => batched,
+ 'batches' => batches,
+ 'batches_count' => 1,
+ 'total_objects_count' => 1
+ }
+ ]
)
end
@@ -190,4 +202,84 @@ RSpec.describe BulkImports::ExportStatus do
end
end
end
+
+ describe 'batching information' do
+ let(:status) { BulkImports::Export::FINISHED }
+
+ describe '#batched?' do
+ context 'when export is batched' do
+ let(:batched) { true }
+
+ it 'returns true' do
+ expect(subject.batched?).to eq(true)
+ end
+ end
+
+ context 'when export is not batched' do
+ it 'returns false' do
+ expect(subject.batched?).to eq(false)
+ end
+ end
+
+ context 'when export batch information is missing' do
+ let(:response_double) do
+ instance_double(HTTParty::Response, parsed_response: [{ 'relation' => 'labels', 'status' => status }])
+ end
+
+ it 'returns false' do
+ expect(subject.batched?).to eq(false)
+ end
+ end
+ end
+
+ describe '#batches_count' do
+ context 'when batches count is present' do
+ it 'returns batches count' do
+ expect(subject.batches_count).to eq(1)
+ end
+ end
+
+ context 'when batches count is missing' do
+ let(:response_double) do
+ instance_double(HTTParty::Response, parsed_response: [{ 'relation' => 'labels', 'status' => status }])
+ end
+
+ it 'returns 0' do
+ expect(subject.batches_count).to eq(0)
+ end
+ end
+ end
+
+ describe '#batch' do
+ context 'when export is batched' do
+ let(:batched) { true }
+ let(:batches) do
+ [
+ { 'relation' => 'labels', 'status' => status, 'batch_number' => 1 },
+ { 'relation' => 'milestones', 'status' => status, 'batch_number' => 2 }
+ ]
+ end
+
+ context 'when batch number is in range' do
+ it 'returns batch information' do
+ expect(subject.batch(1)['relation']).to eq('labels')
+ expect(subject.batch(2)['relation']).to eq('milestones')
+ expect(subject.batch(3)).to eq(nil)
+ end
+ end
+ end
+
+ context 'when batch number is less than 1' do
+ it 'raises error' do
+ expect { subject.batch(0) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when export is not batched' do
+ it 'returns nil' do
+ expect(subject.batch(1)).to eq(nil)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/file_transfer/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
index e50f52c728f..9e1e7cf6d6e 100644
--- a/spec/models/bulk_imports/file_transfer/group_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
@@ -40,7 +40,12 @@ RSpec.describe BulkImports::FileTransfer::GroupConfig, feature_category: :import
describe '#top_relation_tree' do
it 'returns relation tree of a top level relation' do
- expect(subject.top_relation_tree('labels')).to eq('priorities' => {})
+ expect(subject.top_relation_tree('boards')).to include(
+ 'lists' => a_hash_including({
+ 'board' => anything,
+ 'label' => anything
+ })
+ )
end
end
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index c00f46683b9..2e0b13c35ea 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -2,105 +2,93 @@
require 'spec_helper'
-RSpec.describe Ci::ArtifactBlob do
- let_it_be(:project) { create(:project, :public) }
+RSpec.describe Ci::ArtifactBlob, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project, :public, path: 'project1') }
let_it_be(:build) { create(:ci_build, :artifacts, project: project) }
+ let(:pages_port) { nil }
let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/another-subdirectory/banana_sample.gif') }
- subject { described_class.new(entry) }
+ subject(:blob) { described_class.new(entry) }
+
+ before do
+ stub_pages_setting(
+ enabled: true,
+ artifacts_server: true,
+ access_control: true,
+ port: pages_port
+ )
+ end
describe '#id' do
it 'returns a hash of the path' do
- expect(subject.id).to eq(Digest::SHA1.hexdigest(entry.path))
+ expect(blob.id).to eq(Digest::SHA1.hexdigest(entry.path))
end
end
describe '#name' do
it 'returns the entry name' do
- expect(subject.name).to eq(entry.name)
+ expect(blob.name).to eq(entry.name)
end
end
describe '#path' do
it 'returns the entry path' do
- expect(subject.path).to eq(entry.path)
+ expect(blob.path).to eq(entry.path)
end
end
describe '#size' do
it 'returns the entry size' do
- expect(subject.size).to eq(entry.metadata[:size])
+ expect(blob.size).to eq(entry.metadata[:size])
end
end
describe '#mode' do
it 'returns the entry mode' do
- expect(subject.mode).to eq(entry.metadata[:mode])
+ expect(blob.mode).to eq(entry.metadata[:mode])
end
end
describe '#external_storage' do
it 'returns :build_artifact' do
- expect(subject.external_storage).to eq(:build_artifact)
+ expect(blob.external_storage).to eq(:build_artifact)
end
end
describe '#external_url' do
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
- end
+ subject(:url) { blob.external_url(build) }
- describe '.gif extension' do
- it 'returns nil' do
- expect(subject.external_url(build.project, build)).to be_nil
- end
+ context 'with not allowed extension' do
+ it { is_expected.to be_nil }
end
- context 'txt extensions' do
+ context 'with allowed extension' do
let(:path) { 'other_artifacts_0.1.2/doc_sample.txt' }
let(:entry) { build.artifacts_metadata_entry(path) }
- it 'returns a URL' do
- url = subject.external_url(build.project, build)
-
- expect(url).not_to be_nil
- expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
- end
+ it { is_expected.to eq("http://#{project.namespace.path}.example.com/-/project1/-/jobs/#{build.id}/artifacts/other_artifacts_0.1.2/doc_sample.txt") }
context 'when port is configured' do
- let(:port) { 1234 }
-
- it 'returns an URL with port number' do
- allow(Gitlab.config.pages).to receive(:url).and_return("#{Gitlab.config.pages.url}:#{port}")
-
- url = subject.external_url(build.project, build)
+ let(:pages_port) { 1234 }
- expect(url).not_to be_nil
- expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}:#{port}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
- end
+ it { is_expected.to eq("http://#{project.namespace.path}.example.com:1234/-/project1/-/jobs/#{build.id}/artifacts/other_artifacts_0.1.2/doc_sample.txt") }
end
end
end
describe '#external_link?' do
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
- end
-
- context 'gif extensions' do
+ context 'with not allowed extensions' do
it 'returns false' do
- expect(subject.external_link?(build)).to be false
+ expect(blob.external_link?(build)).to be false
end
end
- context 'txt extensions' do
+ context 'with allowed extensions' do
let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
it 'returns true' do
- expect(subject.external_link?(build)).to be true
+ expect(blob.external_link?(build)).to be true
end
end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index ac994735928..d93250af177 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -378,6 +378,91 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
end
end
+ describe '#variables' do
+ it 'returns bridge scoped variables and pipeline persisted variables' do
+ expect(bridge.variables.to_hash)
+ .to eq(bridge.scoped_variables.concat(bridge.pipeline.persisted_variables).to_hash)
+ end
+ end
+
+ describe '#pipeline_variables' do
+ it 'returns the pipeline variables' do
+ expect(bridge.pipeline_variables).to eq(bridge.pipeline.variables)
+ end
+ end
+
+ describe '#pipeline_schedule_variables' do
+ context 'when pipeline is on a schedule' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ let(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
+
+ it 'returns the pipeline schedule variables' do
+ create(:ci_pipeline_schedule_variable, key: 'FOO', value: 'foo', pipeline_schedule: pipeline.pipeline_schedule)
+
+ pipeline_schedule_variables = bridge.reload.pipeline_schedule_variables
+ expect(pipeline_schedule_variables).to match_array([have_attributes({ key: 'FOO', value: 'foo' })])
+ end
+ end
+
+ context 'when pipeline is not on a schedule' do
+ it 'returns empty array' do
+ expect(bridge.pipeline_schedule_variables).to eq([])
+ end
+ end
+ end
+
+ describe '#forward_yaml_variables?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:forward, :result) do
+ true | true
+ false | false
+ nil | true
+ end
+
+ with_them do
+ let(:options) do
+ {
+ trigger: {
+ project: 'my/project',
+ branch: 'master',
+ forward: { yaml_variables: forward }.compact
+ }
+ }
+ end
+
+ let(:bridge) { build(:ci_bridge, options: options) }
+
+ it { expect(bridge.forward_yaml_variables?).to eq(result) }
+ end
+ end
+
+ describe '#forward_pipeline_variables?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:forward, :result) do
+ true | true
+ false | false
+ nil | false
+ end
+
+ with_them do
+ let(:options) do
+ {
+ trigger: {
+ project: 'my/project',
+ branch: 'master',
+ forward: { pipeline_variables: forward }.compact
+ }
+ }
+ end
+
+ let(:bridge) { build(:ci_bridge, options: options) }
+
+ it { expect(bridge.forward_pipeline_variables?).to eq(result) }
+ end
+ end
+
describe 'metadata support' do
it 'reads YAML variables from metadata' do
expect(bridge.yaml_variables).not_to be_empty
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index 0709aa47ff1..ab32234eba3 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildDependencies do
+RSpec.describe Ci::BuildDependencies, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :repository) }
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 8ed0e50e4b0..bd21de1f05a 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildMetadata do
+RSpec.describe Ci::BuildMetadata, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb
index 90426f60c73..8f6c95053c9 100644
--- a/spec/models/ci/build_report_result_spec.rb
+++ b/spec/models/ci/build_report_result_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildReportResult do
+RSpec.describe Ci::BuildReportResult, feature_category: :continuous_integration do
let_it_be_with_reload(:build_report_result) { create(:ci_build_report_result, :with_junit_success) }
it_behaves_like 'cleanup by a loose foreign key' do
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index 002aff25593..dac7edbe6cc 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true, feature_category: :continuou
session = build_with_local_runner_session_url.reload.runner_session
expect(session.errors).to be_empty
- expect(session).to be_a(Ci::BuildRunnerSession)
+ expect(session).to be_a(described_class)
expect(session.url).to eq(url)
end
end
@@ -59,7 +59,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true, feature_category: :continuou
simple_build.save!
session = simple_build.reload.runner_session
- expect(session).to be_a(Ci::BuildRunnerSession)
+ expect(session).to be_a(described_class)
expect(session.url).to eq(url)
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 51cd6efb85f..b7f457962a0 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2282,7 +2282,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe '.keep_artifacts!' do
let!(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline) }
let!(:builds_for_update) do
- Ci::Build.where(id: create_list(:ci_build, 3, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline).map(&:id))
+ described_class.where(id: create_list(:ci_build, 3, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline).map(&:id))
end
it 'resets expire_at' do
@@ -2899,7 +2899,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
{ key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false },
{ key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
{ key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false },
- { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
+ { key: 'CI_PAGES_URL', value: Gitlab::Pages::UrlBuilder.new(project).pages_url, public: true, masked: false },
{ key: 'CI_DEPENDENCY_PROXY_SERVER', value: Gitlab.host_with_port, public: true, masked: false },
{ key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX',
value: "#{Gitlab.host_with_port}/#{project.namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}",
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index 4c1ade5c308..45d49d65b02 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
it { is_expected.to delegate_method(:star_count).to(:project) }
it { is_expected.to delegate_method(:forks_count).to(:project) }
+ it { is_expected.to define_enum_for(:state).with_values({ draft: 0, published: 1 }) }
+
describe '.for_projects' do
it 'returns catalog resources for the given project IDs' do
resources_for_projects = described_class.for_projects(project.id)
@@ -65,4 +67,10 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
expect(resource.latest_version).to eq(release3)
end
end
+
+ describe '#state' do
+ it 'defaults to draft' do
+ expect(resource.state).to eq('draft')
+ 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 6f73d89d760..5de7f5a527f 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DailyBuildGroupReportResult do
+RSpec.describe Ci::DailyBuildGroupReportResult, feature_category: :continuous_integration do
let(:daily_build_group_report_result) { build(:ci_daily_build_group_report_result) }
describe 'associations' do
diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/ci/external_pull_request_spec.rb
index 10136dd0bdb..2a273146626 100644
--- a/spec/models/external_pull_request_spec.rb
+++ b/spec/models/ci/external_pull_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ExternalPullRequest do
+RSpec.describe Ci::ExternalPullRequest, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let(:source_branch) { 'the-branch' }
@@ -228,12 +228,12 @@ RSpec.describe ExternalPullRequest do
it 'returns modified paths' do
expect(modified_paths).to eq ['bar/branch-test.txt',
- 'files/js/commit.coffee',
- 'with space/README.md']
+ 'files/js/commit.coffee',
+ 'with space/README.md']
end
end
- context 'loose foreign key on external_pull_requests.project_id' do
+ context 'with a loose foreign key on external_pull_requests.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
let!(:model) { create(:external_pull_request, project: parent) }
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index 5a8a2b391e1..bf2405a5936 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -13,13 +13,19 @@ RSpec.describe Ci::GroupVariable, feature_category: :secrets_management do
it { is_expected.to include_module(Presentable) }
it { is_expected.to include_module(Ci::Maskable) }
it { is_expected.to include_module(HasEnvironmentScope) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to([:group_id, :environment_scope]).with_message(/\(\w+\) has already been taken/) }
+
+ describe 'validations' do
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to([:group_id, :environment_scope]).with_message(/\(\w+\) has already been taken/) }
+ it { is_expected.to allow_values('').for(:description) }
+ it { is_expected.to allow_values(nil).for(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(255) }
+ end
describe '.by_environment_scope' do
let!(:matching_variable) { create(:ci_group_variable, environment_scope: 'production ') }
let!(:non_matching_variable) { create(:ci_group_variable, environment_scope: 'staging') }
- subject { Ci::GroupVariable.by_environment_scope('production') }
+ subject { described_class.by_environment_scope('production') }
it { is_expected.to contain_exactly(matching_variable) }
end
@@ -84,6 +90,49 @@ RSpec.describe Ci::GroupVariable, feature_category: :secrets_management do
end
end
+ describe 'sort_by_attribute' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:environment_scope) { 'env_scope' }
+ let_it_be(:variable1) { create(:ci_group_variable, key: 'd_var', group: group, environment_scope: environment_scope, created_at: 4.days.ago) }
+ let_it_be(:variable2) { create(:ci_group_variable, key: 'a_var', group: group, environment_scope: environment_scope, created_at: 3.days.ago) }
+ let_it_be(:variable3) { create(:ci_group_variable, key: 'c_var', group: group, environment_scope: environment_scope, created_at: 2.days.ago) }
+ let_it_be(:variable4) { create(:ci_group_variable, key: 'b_var', group: group, environment_scope: environment_scope, created_at: 1.day.ago) }
+
+ let(:sort_by_attribute) { described_class.sort_by_attribute(method).pluck(:key) }
+
+ describe '.created_at_asc' do
+ let(:method) { 'created_at_asc' }
+
+ it 'order by created_at ascending' do
+ expect(sort_by_attribute).to eq(%w[d_var a_var c_var b_var])
+ end
+ end
+
+ describe '.created_at_desc' do
+ let(:method) { 'created_at_desc' }
+
+ it 'order by created_at descending' do
+ expect(sort_by_attribute).to eq(%w[b_var c_var a_var d_var])
+ end
+ end
+
+ describe '.key_asc' do
+ let(:method) { 'key_asc' }
+
+ it 'order by key ascending' do
+ expect(sort_by_attribute).to eq(%w[a_var b_var c_var d_var])
+ end
+ end
+
+ describe '.key_desc' do
+ let(:method) { 'key_desc' }
+
+ it 'order by key descending' do
+ expect(sort_by_attribute).to eq(%w[d_var c_var b_var a_var])
+ end
+ end
+ end
+
it_behaves_like 'cleanup by a loose foreign key' do
let!(:model) { create(:ci_group_variable) }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a34657adf60..83c233fa942 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -204,7 +204,7 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
describe '.associated_file_types_for' do
using RSpec::Parameterized::TableSyntax
- subject { Ci::JobArtifact.associated_file_types_for(file_type) }
+ subject { described_class.associated_file_types_for(file_type) }
where(:file_type, :result) do
'codequality' | %w(codequality)
diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb
index e488580ae7b..ecaa8f59ecf 100644
--- a/spec/models/ci/persistent_ref_spec.rb
+++ b/spec/models/ci/persistent_ref_spec.rb
@@ -3,14 +3,28 @@
require 'spec_helper'
RSpec.describe Ci::PersistentRef do
- it 'cleans up persistent refs after pipeline finished' do
+ it 'cleans up persistent refs after pipeline finished', :sidekiq_inline do
pipeline = create(:ci_pipeline, :running)
- expect(pipeline.persistent_ref).to receive(:delete).once
+ expect(Ci::PipelineCleanupRefWorker).to receive(:perform_async).with(pipeline.id)
pipeline.succeed!
end
+ context 'when pipeline_cleanup_ref_worker_async is disabled' do
+ before do
+ stub_feature_flags(pipeline_cleanup_ref_worker_async: false)
+ end
+
+ it 'cleans up persistent refs after pipeline finished' do
+ pipeline = create(:ci_pipeline, :running)
+
+ expect(pipeline.persistent_ref).to receive(:delete).once
+
+ pipeline.succeed!
+ end
+ end
+
describe '#exist?' do
subject { pipeline.persistent_ref.exist? }
@@ -72,7 +86,7 @@ RSpec.describe Ci::PersistentRef do
describe '#delete' do
subject { pipeline.persistent_ref.delete }
- let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+ let(:pipeline) { create(:ci_pipeline, :success, sha: sha, project: project) }
let(:project) { create(:project, :repository) }
let(:sha) { project.repository.commit.sha }
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index 3038cdc944b..eb89c7af208 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
describe '.report_exists?' do
- subject(:pipeline_artifact) { Ci::PipelineArtifact.report_exists?(file_type) }
+ subject(:pipeline_artifact) { described_class.report_exists?(file_type) }
context 'when file_type is code_coverage' do
let(:file_type) { :code_coverage }
@@ -149,7 +149,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
describe '.find_by_file_type' do
- subject(:pipeline_artifact) { Ci::PipelineArtifact.find_by_file_type(file_type) }
+ subject(:pipeline_artifact) { described_class.find_by_file_type(file_type) }
context 'when file_type is code_coverage' do
let(:file_type) { :code_coverage }
@@ -204,7 +204,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
let(:size) { file['tempfile'].size }
subject do
- Ci::PipelineArtifact.create_or_replace_for_pipeline!(
+ described_class.create_or_replace_for_pipeline!(
pipeline: pipeline,
file_type: file_type,
file: file,
@@ -231,7 +231,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
it "creates a new pipeline artifact with pipeline's locked state" do
- artifact = Ci::PipelineArtifact.create_or_replace_for_pipeline!(
+ artifact = described_class.create_or_replace_for_pipeline!(
pipeline: pipeline,
file_type: file_type,
file: file,
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b9e331affb1..ae3725a0b08 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1328,11 +1328,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
%w[succeed! drop! cancel! skip! block! delay!].each do |action|
context "when the pipeline received #{action} event" do
- it 'deletes a persistent ref' do
- expect(pipeline.persistent_ref).to receive(:delete).once
+ it 'deletes a persistent ref asynchronously', :sidekiq_inline do
+ expect(pipeline.persistent_ref).not_to receive(:delete_refs)
+
+ expect(Ci::PipelineCleanupRefWorker).to receive(:perform_async)
+ .with(pipeline.id).and_call_original
+
+ expect_next_instance_of(Ci::PersistentRef) do |persistent_ref|
+ expect(persistent_ref).to receive(:delete_refs)
+ .with("refs/#{Repository::REF_PIPELINES}/#{pipeline.id}").once
+ end
pipeline.public_send(action)
end
+
+ context 'when pipeline_cleanup_ref_worker_async is disabled' do
+ before do
+ stub_feature_flags(pipeline_cleanup_ref_worker_async: false)
+ end
+
+ it 'deletes a persistent ref synchronously' do
+ expect(Ci::PipelineCleanupRefWorker).not_to receive(:perform_async).with(pipeline.id)
+
+ expect(pipeline.persistent_ref).to receive(:delete_refs).once
+ .with("refs/#{Repository::REF_PIPELINES}/#{pipeline.id}")
+
+ pipeline.public_send(action)
+ end
+ end
end
end
@@ -5323,7 +5346,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
it 'raises an exception' do
pipeline.save!
- pipeline_id = Ci::Pipeline.where(id: pipeline.id).select(:id).first
+ pipeline_id = described_class.where(id: pipeline.id).select(:id).first
expect { pipeline_id.age_in_minutes }.to raise_error(ArgumentError)
end
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 86894ebcf2d..e1c449e18ac 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
describe 'delegations' do
- subject { Ci::Processable.new }
+ subject { described_class.new }
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
@@ -401,7 +401,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let!(:another_build) { create(:ci_build, project: project) }
before do
- Ci::Processable.update_all(scheduling_type: nil)
+ described_class.update_all(scheduling_type: nil)
end
it 'populates scheduling_type of processables' do
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
index eab5a40bc30..a60aed98a21 100644
--- a/spec/models/ci/ref_spec.rb
+++ b/spec/models/ci/ref_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe Ci::Ref do
it 'returns an existing ci_ref' do
expect { subject }.not_to change { described_class.count }
- expect(subject).to eq(Ci::Ref.find_by(project_id: project.id, ref_path: expected_ref_path))
+ expect(subject).to eq(described_class.find_by(project_id: project.id, ref_path: expected_ref_path))
end
end
@@ -84,7 +84,7 @@ RSpec.describe Ci::Ref do
it 'creates a new ci_ref' do
expect { subject }.to change { described_class.count }.by(1)
- expect(subject).to eq(Ci::Ref.find_by(project_id: project.id, ref_path: expected_ref_path))
+ expect(subject).to eq(described_class.find_by(project_id: project.id, ref_path: expected_ref_path))
end
end
end
diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb
index d69c9ef845e..80cffb98dff 100644
--- a/spec/models/ci/runner_manager_spec.rb
+++ b/spec/models/ci/runner_manager_spec.rb
@@ -69,6 +69,49 @@ RSpec.describe Ci::RunnerManager, feature_category: :runner_fleet, type: :model
it { is_expected.to eq(7.days.ago) }
end
+ describe '.for_runner' do
+ subject(:runner_managers) { described_class.for_runner(runner_arg) }
+
+ let_it_be(:runner1) { create(:ci_runner) }
+ let_it_be(:runner_manager11) { create(:ci_runner_machine, runner: runner1) }
+ let_it_be(:runner_manager12) { create(:ci_runner_machine, runner: runner1) }
+
+ context 'with single runner' do
+ let(:runner_arg) { runner1 }
+
+ it { is_expected.to contain_exactly(runner_manager11, runner_manager12) }
+ end
+
+ context 'with multiple runners' do
+ let(:runner_arg) { [runner1, runner2] }
+
+ let_it_be(:runner2) { create(:ci_runner) }
+ let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner2) }
+
+ it { is_expected.to contain_exactly(runner_manager11, runner_manager12, runner_manager2) }
+ end
+ end
+
+ describe '.aggregate_upgrade_status_by_runner_id' do
+ let!(:runner_version1) { create(:ci_runner_version, version: '16.0.0', status: :recommended) }
+ let!(:runner_version2) { create(:ci_runner_version, version: '16.0.1', status: :available) }
+
+ let!(:runner1) { create(:ci_runner) }
+ let!(:runner2) { create(:ci_runner) }
+ let!(:runner_manager11) { create(:ci_runner_machine, runner: runner1, version: runner_version1.version) }
+ let!(:runner_manager12) { create(:ci_runner_machine, runner: runner1, version: runner_version2.version) }
+ let!(:runner_manager2) { create(:ci_runner_machine, runner: runner2, version: runner_version2.version) }
+
+ subject { described_class.aggregate_upgrade_status_by_runner_id }
+
+ it 'contains aggregate runner upgrade status by runner ID' do
+ is_expected.to eq({
+ runner1.id => :recommended,
+ runner2.id => :available
+ })
+ end
+ end
+
describe '#status', :freeze_time do
let(:runner_manager) { build(:ci_runner_machine, created_at: 8.days.ago) }
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index b0ff070e4a6..50e2ded695c 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -316,8 +316,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'when use_traversal_ids* are disabled' do
before do
stub_feature_flags(
- use_traversal_ids: false,
- use_traversal_ids_for_ancestors: false
+ use_traversal_ids: false
)
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 79e92082ee1..1be50083cd4 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Stage, :models do
+RSpec.describe Ci::Stage, :models, feature_category: :continuous_integration do
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
let(:stage) { create(:ci_stage, pipeline: pipeline, project: pipeline.project) }
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 85327dbeb34..c93f355718a 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -15,13 +15,16 @@ RSpec.describe Ci::Variable, feature_category: :secrets_management do
it { is_expected.to include_module(Ci::Maskable) }
it { is_expected.to include_module(HasEnvironmentScope) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
+ it { is_expected.to allow_values('').for(:description) }
+ it { is_expected.to allow_values(nil).for(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(255) }
end
describe '.by_environment_scope' do
let!(:matching_variable) { create(:ci_variable, environment_scope: 'production ') }
let!(:non_matching_variable) { create(:ci_variable, environment_scope: 'staging') }
- subject { Ci::Variable.by_environment_scope('production') }
+ subject { described_class.by_environment_scope('production') }
it { is_expected.to contain_exactly(matching_variable) }
end
diff --git a/spec/models/ci_platform_metric_spec.rb b/spec/models/ci_platform_metric_spec.rb
index e59730792b8..1f25f10c5d2 100644
--- a/spec/models/ci_platform_metric_spec.rb
+++ b/spec/models/ci_platform_metric_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe CiPlatformMetric, feature_category: :continuous_integration do
subject { build(:ci_platform_metric) }
- it_behaves_like 'a BulkInsertSafe model', CiPlatformMetric do
+ it_behaves_like 'a BulkInsertSafe model', described_class do
let(:valid_items_for_bulk_insertion) { build_list(:ci_platform_metric, 10) }
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any non-constraint validations defined
end
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index 7c546f42d5d..6201b7b1861 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -198,14 +198,6 @@ RSpec.describe Clusters::Agent, feature_category: :deployment_management do
it { is_expected.to eq(allowed) }
end
-
- context 'when expose_authorized_cluster_agents feature flag is disabled' do
- before do
- stub_feature_flags(expose_authorized_cluster_agents: false)
- end
-
- it { is_expected.to eq(false) }
- end
end
context 'with group-level authorization' do
@@ -226,14 +218,6 @@ RSpec.describe Clusters::Agent, feature_category: :deployment_management do
it { is_expected.to eq(allowed) }
end
-
- context 'when expose_authorized_cluster_agents feature flag is disabled' do
- before do
- stub_feature_flags(expose_authorized_cluster_agents: false)
- end
-
- it { is_expected.to eq(false) }
- end
end
end
@@ -269,14 +253,6 @@ RSpec.describe Clusters::Agent, feature_category: :deployment_management do
it { is_expected.to eq(allowed) }
end
-
- context 'when expose_authorized_cluster_agents feature flag is disabled' do
- before do
- stub_feature_flags(expose_authorized_cluster_agents: false)
- end
-
- it { is_expected.to eq(false) }
- end
end
context 'with group-level authorization' do
@@ -297,14 +273,6 @@ RSpec.describe Clusters::Agent, feature_category: :deployment_management do
it { is_expected.to eq(allowed) }
end
-
- context 'when expose_authorized_cluster_agents feature flag is disabled' do
- before do
- stub_feature_flags(expose_authorized_cluster_agents: false)
- end
-
- it { is_expected.to eq(false) }
- end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 99932dc27d1..73df283d996 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -983,7 +983,7 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
end
describe '#make_cleanup_errored!' do
- non_errored_states = Clusters::Cluster.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
+ non_errored_states = described_class.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
non_errored_states.each do |state|
it "transitions cleanup_status from #{state} to cleanup_errored" do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index edb856d34df..dd3d4f1865c 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Commit do
it 'returns a Commit' do
commit = described_class.build_from_sidekiq_hash(project, id: '123')
- expect(commit).to be_an_instance_of(Commit)
+ expect(commit).to be_an_instance_of(described_class)
end
it 'parses date strings into Time instances' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 38c45e8c975..ac356bcd65a 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
let!(:stale_scheduled) { create(:commit_status, scheduled_at: 1.day.ago) }
let!(:fresh_scheduled) { create(:commit_status, scheduled_at: 1.minute.ago) }
- subject { CommitStatus.scheduled_at_before(1.hour.ago) }
+ subject { described_class.scheduled_at_before(1.hour.ago) }
it { is_expected.to contain_exactly(stale_scheduled) }
end
@@ -141,7 +141,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
commit_status.update!(retried: false, status: :pending)
# another process does mark object as processed
- CommitStatus.find(commit_status.id).update_column(:processed, true)
+ described_class.find(commit_status.id).update_column(:processed, true)
# subsequent status transitions on the same instance
# always saves processed=false to DB even though
@@ -149,7 +149,7 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
commit_status.update!(retried: false, status: :running)
# we look at a persisted state in DB
- expect(CommitStatus.find(commit_status.id).processed).to eq(false)
+ expect(described_class.find(commit_status.id).processed).to eq(false)
end
end
diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
index e8d84fe9630..256cd386ce2 100644
--- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
+++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe BatchDestroyDependentAssociations do
let_it_be(:build) { create(:ci_build, project: project) }
let_it_be(:notification_setting) { create(:notification_setting, project: project) }
let_it_be(:note) { create(:note, project: project) }
- let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:merge_request) { create(:merge_request, :skip_diff_creation, source_project: project) }
it 'destroys multiple notes' do
create(:note, project: project)
diff --git a/spec/models/concerns/counter_attribute_spec.rb b/spec/models/concerns/counter_attribute_spec.rb
index c8224c64ba2..cde58e4088c 100644
--- a/spec/models/concerns/counter_attribute_spec.rb
+++ b/spec/models/concerns/counter_attribute_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe CounterAttribute, :counter_attribute, :clean_gitlab_redis_shared_
let(:project_statistics) { create(:project_statistics) }
let(:model) { CounterAttributeModel.find(project_statistics.id) }
- it_behaves_like CounterAttribute, [:build_artifacts_size, :commit_count, :packages_size] do
+ it_behaves_like described_class, [:build_artifacts_size, :commit_count, :packages_size] do
let(:model) { CounterAttributeModel.find(project_statistics.id) }
end
diff --git a/spec/models/concerns/database_event_tracking_spec.rb b/spec/models/concerns/database_event_tracking_spec.rb
index 502cecaaf76..a99b4737537 100644
--- a/spec/models/concerns/database_event_tracking_spec.rb
+++ b/spec/models/concerns/database_event_tracking_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DatabaseEventTracking, :snowplow do
+RSpec.describe DatabaseEventTracking, :snowplow, feature_category: :service_ping do
before do
allow(Gitlab::Tracking).to receive(:database_event).and_call_original
end
@@ -31,18 +31,6 @@ RSpec.describe DatabaseEventTracking, :snowplow do
end
end
- context 'if product_intelligence_database_event_tracking FF is off' do
- before do
- stub_feature_flags(product_intelligence_database_event_tracking: false)
- end
-
- it 'does not track the event' do
- create_test_class_record
-
- expect_no_snowplow_event(tracking_method: :database_event)
- end
- end
-
describe 'event tracking' do
let(:category) { test_class.to_s }
let(:event) { 'database_event' }
diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb
index 68a25917ce1..78fe265a6bb 100644
--- a/spec/models/concerns/expirable_spec.rb
+++ b/spec/models/concerns/expirable_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Expirable do
end
describe '.expired' do
- it { expect(ProjectMember.expired).to match_array([expired]) }
+ it { expect(ProjectMember.expired).to contain_exactly(expired) }
it 'scopes the query when multiple models are expirable' do
expired_access_token = create(:personal_access_token, :expired, user: no_expire.user)
diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb
index d593d829dca..f27f3a5e6a0 100644
--- a/spec/models/concerns/group_descendant_spec.rb
+++ b/spec/models/concerns/group_descendant_spec.rb
@@ -19,16 +19,15 @@ RSpec.describe GroupDescendant do
query_count = ActiveRecord::QueryRecorder.new { test_group.hierarchy }.count
- # use_traversal_ids_for_ancestors_upto actor based feature flag check adds an extra query.
- expect(query_count).to eq(2)
+ expect(query_count).to eq(1)
end
it 'only queries once for the ancestors when a top is given' do
test_group = create(:group, parent: subsub_group).reload
recorder = ActiveRecord::QueryRecorder.new { test_group.hierarchy(subgroup) }
- # use_traversal_ids_for_ancestors_upto actor based feature flag check adds an extra query.
- expect(recorder.count).to eq(2)
+
+ expect(recorder.count).to eq(1)
end
it 'builds a hierarchy for a group' do
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index f9bf576d75b..49c3d11ed6b 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe User, feature_category: :system_access do
describe 'validations' do
it 'validates type presence' do
- expect(User.new).to validate_presence_of(:user_type)
+ expect(described_class.new).to validate_presence_of(:user_type)
end
end
diff --git a/spec/models/concerns/integrations/enable_ssl_verification_spec.rb b/spec/models/concerns/integrations/enable_ssl_verification_spec.rb
index 802e950c0c2..418f3f4dbc6 100644
--- a/spec/models/concerns/integrations/enable_ssl_verification_spec.rb
+++ b/spec/models/concerns/integrations/enable_ssl_verification_spec.rb
@@ -19,5 +19,5 @@ RSpec.describe Integrations::EnableSslVerification do
let(:integration) { described_class.new }
- include_context Integrations::EnableSslVerification
+ include_context described_class
end
diff --git a/spec/models/concerns/integrations/reset_secret_fields_spec.rb b/spec/models/concerns/integrations/reset_secret_fields_spec.rb
index a372550c70f..3b15b95fea9 100644
--- a/spec/models/concerns/integrations/reset_secret_fields_spec.rb
+++ b/spec/models/concerns/integrations/reset_secret_fields_spec.rb
@@ -15,5 +15,5 @@ RSpec.describe Integrations::ResetSecretFields do
let(:integration) { described_class.new }
- it_behaves_like Integrations::ResetSecretFields
+ it_behaves_like described_class
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4e99419a7f2..e4af778b967 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable do
+RSpec.describe Issuable, feature_category: :team_planning do
include ProjectForksHelper
using RSpec::Parameterized::TableSyntax
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 46a876f34e9..01efe66a419 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -356,4 +356,20 @@ RSpec.describe Milestone, 'Milestoneish', factory_default: :keep do
expect(milestone.human_total_time_estimate).to be_nil
end
end
+
+ describe '#expires_at' do
+ it 'returns the date when milestone expires' do
+ due_date = Date.today + 1.day
+ milestone.due_date = due_date
+
+ expect(milestone.expires_at).to eq("expires on #{due_date.to_fs(:medium)}")
+ end
+
+ it 'returns the date when milestone expires' do
+ due_date = Date.today - 1.day
+ milestone.due_date = due_date
+
+ expect(milestone.expires_at).to eq("expired on #{due_date.to_fs(:medium)}")
+ end
+ end
end
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
index 69c58a5cfe5..09646f6c4eb 100644
--- a/spec/models/concerns/resolvable_note_spec.rb
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -18,25 +18,25 @@ RSpec.describe Note, ResolvableNote do
describe '.potentially_resolvable' do
it 'includes diff and discussion notes on merge requests' do
- expect(Note.potentially_resolvable).to match_array([note3, note4, note6])
+ expect(described_class.potentially_resolvable).to match_array([note3, note4, note6])
end
end
describe '.resolvable' do
it 'includes non-system diff and discussion notes on merge requests' do
- expect(Note.resolvable).to match_array([note3, note4])
+ expect(described_class.resolvable).to match_array([note3, note4])
end
end
describe '.resolved' do
it 'includes resolved non-system diff and discussion notes on merge requests' do
- expect(Note.resolved).to match_array([note3])
+ expect(described_class.resolved).to match_array([note3])
end
end
describe '.unresolved' do
it 'includes non-resolved non-system diff and discussion notes on merge requests' do
- expect(Note.unresolved).to match_array([note4])
+ expect(described_class.unresolved).to match_array([note4])
end
end
end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index 8a2fa6675e5..7ef0473aea8 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -71,8 +71,8 @@ RSpec.describe Spammable, feature_category: :instance_resiliency do
expect(issue.check_for_spam?(user: issue.author)).to eq(true)
end
- it 'returns false for other visibility levels' do
- expect(issue.check_for_spam?(user: issue.author)).to eq(false)
+ it 'returns true for other visibility levels' do
+ expect(issue.check_for_spam?(user: issue.author)).to eq(true)
end
end
@@ -82,7 +82,7 @@ RSpec.describe Spammable, feature_category: :instance_resiliency do
end
context 'when the model is spam' do
- where(model: [:issue, :merge_request, :snippet, :spammable_model])
+ where(model: [:issue, :merge_request, :note, :snippet, :spammable_model])
with_them do
subject do
@@ -94,21 +94,7 @@ RSpec.describe Spammable, feature_category: :instance_resiliency do
it 'has an error related to spam on the model' do
expect(subject.errors.messages[:base])
- .to match_array /Your #{subject.class.model_name.human.downcase} has been recognized as spam./
- end
- end
-
- context 'when the spammable model is a Note' do
- subject do
- Note.new.tap do |m|
- m.spam!
- m.invalidate_if_spam
- end
- end
-
- it 'has an error related to spam on the model' do
- expect(subject.errors.messages[:base])
- .to match_array /Your comment has been recognized as spam./
+ .to match_array /Your #{subject.spammable_entity_type} has been recognized as spam./
end
end
end
@@ -293,7 +279,9 @@ RSpec.describe Spammable, feature_category: :instance_resiliency do
end
describe '#allow_possible_spam?' do
- subject { issue.allow_possible_spam? }
+ let_it_be(:user) { build(:user) }
+
+ subject { spammable_model.allow_possible_spam?(user) }
context 'when the `allow_possible_spam` application setting is turned off' do
it { is_expected.to eq(false) }
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 70123eaac26..cbfc1df64f1 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -112,8 +112,8 @@ RSpec.describe PersonalAccessToken, 'TokenAuthenticatable' do
it 'sets new token' do
subject
- expect(personal_access_token.token).to eq("#{PersonalAccessToken.token_prefix}#{token_value}")
- expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256("#{PersonalAccessToken.token_prefix}#{token_value}"))
+ expect(personal_access_token.token).to eq("#{described_class.token_prefix}#{token_value}")
+ expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256("#{described_class.token_prefix}#{token_value}"))
end
end
@@ -138,7 +138,7 @@ RSpec.describe PersonalAccessToken, 'TokenAuthenticatable' do
end
describe '.find_by_token' do
- subject { PersonalAccessToken.find_by_token(token_value) }
+ subject { described_class.find_by_token(token_value) }
it 'finds the token' do
personal_access_token.save!
@@ -347,7 +347,7 @@ RSpec.describe Ci::Runner, 'TokenAuthenticatable', :freeze_time do
end
describe '.find_by_token' do
- subject { Ci::Runner.find_by_token(runner.token) }
+ subject { described_class.find_by_token(runner.token) }
context 'when runner has no token expiration' do
let(:runner) { non_expirable_runner }
diff --git a/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb b/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb
index 0a71699971e..842020896d9 100644
--- a/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb
+++ b/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe VulnerabilityFindingSignatureHelpers do
describe '#priority' do
it 'returns numeric values of the priority string' do
+ expect(cls.new('scope_offset_compressed').priority).to eq(4)
expect(cls.new('scope_offset').priority).to eq(3)
expect(cls.new('location').priority).to eq(2)
expect(cls.new('hash').priority).to eq(1)
@@ -24,6 +25,7 @@ RSpec.describe VulnerabilityFindingSignatureHelpers do
describe '#self.priority' do
it 'returns the numeric value of the provided string' do
+ expect(cls.priority('scope_offset_compressed')).to eq(4)
expect(cls.priority('scope_offset')).to eq(3)
expect(cls.priority('location')).to eq(2)
expect(cls.priority('hash')).to eq(1)
diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb
index b88eddf19dc..e5f9fdd410e 100644
--- a/spec/models/container_expiration_policy_spec.rb
+++ b/spec/models/container_expiration_policy_spec.rb
@@ -11,8 +11,7 @@ RSpec.describe ContainerExpirationPolicy, type: :model do
it { is_expected.to validate_presence_of(:project) }
describe '#enabled' do
- it { is_expected.to allow_value(true).for(:enabled) }
- it { is_expected.to allow_value(false).for(:enabled) }
+ it { is_expected.to allow_value(true, false).for(:enabled) }
it { is_expected.not_to allow_value(nil).for(:enabled) }
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index d8019e74c71..93fe070e5c4 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -810,7 +810,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.size }
before do
- allow(::Gitlab).to receive(:com?).and_return(on_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(on_com)
allow(repository).to receive(:created_at).and_return(created_at)
end
@@ -1568,7 +1568,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
context 'on gitlab.com' do
before do
- allow(::Gitlab).to receive(:com?).and_return(true)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(true)
end
it { is_expected.to eq(true) }
@@ -1576,7 +1576,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
context 'not on gitlab.com' do
before do
- allow(::Gitlab).to receive(:com?).and_return(false)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(false)
end
it { is_expected.to eq(false) }
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 6beb5323f60..3d78a9089ca 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -127,7 +127,7 @@ RSpec.describe CustomerRelations::Contact, type: :model do
before do
old_root_group.update!(parent: new_root_group)
- CustomerRelations::Contact.move_to_root_group(old_root_group)
+ described_class.move_to_root_group(old_root_group)
end
it 'moves contacts with unique emails and deletes the rest' do
diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb
index 350a4e613c6..8151bf18bed 100644
--- a/spec/models/customer_relations/organization_spec.rb
+++ b/spec/models/customer_relations/organization_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe CustomerRelations::Organization, type: :model do
before do
old_root_group.update!(parent: new_root_group)
- CustomerRelations::Organization.move_to_root_group(old_root_group)
+ described_class.move_to_root_group(old_root_group)
end
it 'moves organizations with unique names and deletes the rest' do
diff --git a/spec/models/dependency_proxy/image_ttl_group_policy_spec.rb b/spec/models/dependency_proxy/image_ttl_group_policy_spec.rb
index 9f6358e1286..a58e8df45e4 100644
--- a/spec/models/dependency_proxy/image_ttl_group_policy_spec.rb
+++ b/spec/models/dependency_proxy/image_ttl_group_policy_spec.rb
@@ -11,8 +11,7 @@ RSpec.describe DependencyProxy::ImageTtlGroupPolicy, type: :model do
it { is_expected.to validate_presence_of(:group) }
describe '#enabled' do
- it { is_expected.to allow_value(true).for(:enabled) }
- it { is_expected.to allow_value(false).for(:enabled) }
+ it { is_expected.to allow_value(true, false).for(:enabled) }
it { is_expected.not_to allow_value(nil).for(:enabled) }
end
diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb
index d43079f607a..174fa400d72 100644
--- a/spec/models/dependency_proxy/manifest_spec.rb
+++ b/spec/models/dependency_proxy/manifest_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
let_it_be(:file_name) { 'foo' }
let_it_be(:digest) { 'bar' }
- subject { DependencyProxy::Manifest.find_by_file_name_or_digest(file_name: file_name, digest: digest) }
+ subject { described_class.find_by_file_name_or_digest(file_name: file_name, digest: digest) }
context 'no manifest exists' do
it { is_expected.to be_nil }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index ede96d79656..227ac69133b 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -650,7 +650,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
context 'when there are no deployments and builds' do
it do
- expect(subject_method(environment)).to eq(Deployment.none)
+ expect(subject_method(environment)).to eq(described_class.none)
end
end
@@ -663,7 +663,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
end
it do
- expect(subject_method(environment)).to eq(Deployment.none)
+ expect(subject_method(environment)).to eq(described_class.none)
end
end
@@ -1285,7 +1285,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:build_status) { :created }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"create\"} }
+ let(:error_message) { %{Status cannot transition via \"create\"} }
end
end
@@ -1315,7 +1315,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:build_status) { :created }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"create\"} }
+ let(:error_message) { %{Status cannot transition via \"create\"} }
end
end
@@ -1323,7 +1323,7 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do
let(:build_status) { :running }
it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"run\"} }
+ let(:error_message) { %{Status cannot transition via \"run\"} }
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 7b7b92a0b8d..066763645ab 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
it { is_expected.to be_kind_of(ReactiveCaching) }
it { is_expected.to nullify_if_blank(:external_url) }
+ it { is_expected.to nullify_if_blank(:kubernetes_namespace) }
it { is_expected.to belong_to(:project).required }
it { is_expected.to belong_to(:merge_request).optional }
@@ -36,6 +37,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
it { is_expected.to validate_length_of(:slug).is_at_most(24) }
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
+ it { is_expected.to validate_length_of(:kubernetes_namespace).is_at_most(63) }
describe 'validation' do
it 'does not become invalid record when external_url is empty' do
@@ -80,15 +82,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
expect(env).to validate_presence_of(:tier).on(:update)
end
end
-
- context 'when FF is disabled' do
- before do
- stub_feature_flags(validate_environment_tier_presence: false)
- end
-
- it { expect(env).to validate_presence_of(:tier).on(:create) }
- it { expect(env).not_to validate_presence_of(:tier).on(:update) }
- end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 527ee96ca86..01fd17bfe10 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -703,10 +703,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
- describe '#ancestors_upto' do
- it { expect(group.ancestors_upto.to_sql).not_to include "WITH ORDINALITY" }
- end
-
describe '.shortest_traversal_ids_prefixes' do
it { expect { described_class.shortest_traversal_ids_prefixes }.to raise_error /Feature not supported since the `:use_traversal_ids` is disabled/ }
end
@@ -737,14 +733,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
it 'hierarchy order' do
expect(group.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC'
end
-
- context 'ancestor linear queries feature flag disabled' do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestors: false)
- end
-
- it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
- end
end
describe '#ancestors_upto' do
@@ -856,7 +844,29 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
it 'returns groups without integration' do
- expect(Group.without_integration(instance_integration)).to contain_exactly(another_group)
+ expect(described_class.without_integration(instance_integration)).to contain_exactly(another_group)
+ end
+ end
+
+ describe '.execute_integrations' do
+ let(:integration) { create(:integrations_slack, :group, group: group) }
+ let(:test_data) { { 'foo' => 'bar' } }
+
+ before do
+ allow(group.integrations).to receive(:public_send).and_return([])
+ allow(group.integrations).to receive(:public_send).with(:push_hooks).and_return([integration])
+ end
+
+ it 'executes integrations with a matching scope' do
+ expect(integration).to receive(:async_execute).with(test_data)
+
+ group.execute_integrations(test_data, :push_hooks)
+ end
+
+ it 'ignores integrations without a matching scope' do
+ expect(integration).not_to receive(:async_execute).with(test_data)
+
+ group.execute_integrations(test_data, :note_hooks)
end
end
@@ -1848,22 +1858,29 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
context 'user-related methods' do
- let(:user_a) { create(:user) }
- let(:user_b) { create(:user) }
- let(:user_c) { create(:user) }
- let(:user_d) { create(:user) }
+ let_it_be(:user_a) { create(:user) }
+ let_it_be(:user_b) { create(:user) }
+ let_it_be(:user_c) { create(:user) }
+ let_it_be(:user_d) { create(:user) }
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
- let(:deep_nested_group) { create(:group, parent: nested_group) }
- let(:project) { create(:project, namespace: group) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:nested_group) { create(:group, parent: group) }
+ let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
+ let_it_be(:project) { create(:project, namespace: group) }
- before do
+ let_it_be(:another_group) { create(:group) }
+ let_it_be(:another_user) { create(:user) }
+
+ before_all do
group.add_developer(user_a)
group.add_developer(user_c)
nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a)
project.add_developer(user_d)
+
+ another_group.add_developer(another_user)
+
+ create(:group_group_link, shared_group: group, shared_with_group: another_group)
end
describe '#direct_and_indirect_users' do
@@ -1876,6 +1893,13 @@ RSpec.describe Group, feature_category: :groups_and_projects do
it 'does not return members of projects belonging to ancestor groups' do
expect(nested_group.direct_and_indirect_users).not_to include(user_d)
end
+
+ context 'when share_with_groups is true' do
+ it 'also returns members of groups invited to this group' do
+ expect(group.direct_and_indirect_users(share_with_groups: true))
+ .to contain_exactly(user_a, user_b, user_c, user_d, another_user)
+ end
+ end
end
describe '#direct_and_indirect_users_with_inactive' do
@@ -2643,232 +2667,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
- describe '#update_shared_runners_setting!' do
- context 'enabled' do
- subject { group.update_shared_runners_setting!('enabled') }
-
- context 'group that its ancestors have shared runners disabled' do
- let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled) }
- let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, parent: parent) }
- let_it_be(:project, reload: true) { create(:project, shared_runners_enabled: false, group: group) }
-
- it 'raises exception' do
- expect { subject }
- .to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
- end
-
- it 'does not enable shared runners' do
- expect do
- begin
- subject
- rescue StandardError
- nil
- end
-
- parent.reload
- group.reload
- project.reload
- end.to not_change { parent.shared_runners_enabled }
- .and not_change { group.shared_runners_enabled }
- .and not_change { project.shared_runners_enabled }
- end
- end
-
- context 'root group with shared runners disabled' do
- let_it_be(:group) { create(:group, :shared_runners_disabled) }
- let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
- let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
-
- it 'enables shared Runners only for itself' do
- expect { subject_and_reload(group, sub_group, project) }
- .to change { group.shared_runners_enabled }.from(false).to(true)
- .and not_change { sub_group.shared_runners_enabled }
- .and not_change { project.shared_runners_enabled }
- end
- end
- end
-
- context 'disabled_and_unoverridable' do
- let_it_be(:group) { create(:group) }
- let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
- let_it_be(:sub_group_2) { create(:group, parent: group) }
- let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) }
- let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) }
-
- subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_UNOVERRIDABLE) }
-
- it 'disables shared Runners for all descendant groups and projects' do
- expect { subject_and_reload(group, sub_group, sub_group_2, project, project_2) }
- .to change { group.shared_runners_enabled }.from(true).to(false)
- .and not_change { group.allow_descendants_override_disabled_shared_runners }
- .and not_change { sub_group.shared_runners_enabled }
- .and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
- .and change { sub_group_2.shared_runners_enabled }.from(true).to(false)
- .and not_change { sub_group_2.allow_descendants_override_disabled_shared_runners }
- .and change { project.shared_runners_enabled }.from(true).to(false)
- .and change { project_2.shared_runners_enabled }.from(true).to(false)
- end
-
- context 'with override on self' do
- let_it_be(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
-
- it 'disables it' do
- expect { subject_and_reload(group) }
- .to not_change { group.shared_runners_enabled }
- .and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
- end
- end
- end
-
- context 'disabled_and_overridable' do
- subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_OVERRIDABLE) }
-
- context 'top level group' do
- let_it_be(:group) { create(:group, :shared_runners_disabled) }
- let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
- let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
-
- it 'enables allow descendants to override only for itself' do
- expect { subject_and_reload(group, sub_group, project) }
- .to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
- .and not_change { group.shared_runners_enabled }
- .and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
- .and not_change { sub_group.shared_runners_enabled }
- .and not_change { project.shared_runners_enabled }
- end
- end
-
- context 'group that its ancestors have shared Runners disabled but allows to override' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
- let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
- let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) }
-
- it 'enables allow descendants to override' do
- expect { subject_and_reload(parent, group, project) }
- .to not_change { parent.allow_descendants_override_disabled_shared_runners }
- .and not_change { parent.shared_runners_enabled }
- .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
- .and not_change { group.shared_runners_enabled }
- .and not_change { project.shared_runners_enabled }
- end
- end
-
- context 'when parent does not allow' do
- let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
- let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
-
- it 'raises exception' do
- expect { subject }
- .to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
- end
-
- it 'does not allow descendants to override' do
- expect do
- begin
- subject
- rescue StandardError
- nil
- end
-
- parent.reload
- group.reload
- end.to not_change { parent.allow_descendants_override_disabled_shared_runners }
- .and not_change { parent.shared_runners_enabled }
- .and not_change { group.allow_descendants_override_disabled_shared_runners }
- .and not_change { group.shared_runners_enabled }
- end
- end
-
- context 'top level group that has shared Runners enabled' do
- let_it_be(:group) { create(:group, shared_runners_enabled: true) }
- let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
- let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
-
- it 'enables allow descendants to override & disables shared runners everywhere' do
- expect { subject_and_reload(group, sub_group, project) }
- .to change { group.shared_runners_enabled }.from(true).to(false)
- .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
- .and change { sub_group.shared_runners_enabled }.from(true).to(false)
- .and change { project.shared_runners_enabled }.from(true).to(false)
- end
- end
- end
-
- context 'disabled_with_override (deprecated)' do
- subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_WITH_OVERRIDE) }
-
- context 'top level group' do
- let_it_be(:group) { create(:group, :shared_runners_disabled) }
- let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
- let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
-
- it 'enables allow descendants to override only for itself' do
- expect { subject_and_reload(group, sub_group, project) }
- .to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
- .and not_change { group.shared_runners_enabled }
- .and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
- .and not_change { sub_group.shared_runners_enabled }
- .and not_change { project.shared_runners_enabled }
- end
- end
-
- context 'group that its ancestors have shared Runners disabled but allows to override' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
- let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
- let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) }
-
- it 'enables allow descendants to override' do
- expect { subject_and_reload(parent, group, project) }
- .to not_change { parent.allow_descendants_override_disabled_shared_runners }
- .and not_change { parent.shared_runners_enabled }
- .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
- .and not_change { group.shared_runners_enabled }
- .and not_change { project.shared_runners_enabled }
- end
- end
-
- context 'when parent does not allow' do
- let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
- let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
-
- it 'raises exception' do
- expect { subject }
- .to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
- end
-
- it 'does not allow descendants to override' do
- expect do
- begin
- subject
- rescue StandardError
- nil
- end
-
- parent.reload
- group.reload
- end.to not_change { parent.allow_descendants_override_disabled_shared_runners }
- .and not_change { parent.shared_runners_enabled }
- .and not_change { group.allow_descendants_override_disabled_shared_runners }
- .and not_change { group.shared_runners_enabled }
- end
- end
-
- context 'top level group that has shared Runners enabled' do
- let_it_be(:group) { create(:group, shared_runners_enabled: true) }
- let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
- let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
-
- it 'enables allow descendants to override & disables shared runners everywhere' do
- expect { subject_and_reload(group, sub_group, project) }
- .to change { group.shared_runners_enabled }.from(true).to(false)
- .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
- .and change { sub_group.shared_runners_enabled }.from(true).to(false)
- .and change { project.shared_runners_enabled }.from(true).to(false)
- end
- end
- end
- end
-
describe "#default_branch_name" do
context "when group.namespace_settings does not have a default branch name" do
it "returns nil" do
diff --git a/spec/models/import_failure_spec.rb b/spec/models/import_failure_spec.rb
index a8ada156dd7..6da4045bc00 100644
--- a/spec/models/import_failure_spec.rb
+++ b/spec/models/import_failure_spec.rb
@@ -12,15 +12,15 @@ RSpec.describe ImportFailure do
let_it_be(:unrelated_failure) { create(:import_failure, project: project) }
it 'returns failures with external_identifiers' do
- expect(ImportFailure.with_external_identifiers).to match_array([github_import_failure])
+ expect(described_class.with_external_identifiers).to match_array([github_import_failure])
end
it 'returns failures for the given correlation ID' do
- expect(ImportFailure.failures_by_correlation_id(correlation_id)).to match_array([hard_failure, soft_failure])
+ expect(described_class.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])
+ expect(described_class.hard_failures_by_correlation_id(correlation_id)).to eq([hard_failure])
end
it 'orders hard failures by newest first' do
diff --git a/spec/models/incident_management/timeline_event_spec.rb b/spec/models/incident_management/timeline_event_spec.rb
index 036f5affb87..40adf46e7d5 100644
--- a/spec/models/incident_management/timeline_event_spec.rb
+++ b/spec/models/incident_management/timeline_event_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe IncidentManagement::TimelineEvent do
end
let(:expected_note_html) do
- %Q(<p>note <strong>bold</strong> <em>italic</em> <code>code</code> #{expected_image_html} #{expected_emoji_html}</p>)
+ %(<p>note <strong>bold</strong> <em>italic</em> <code>code</code> #{expected_image_html} #{expected_emoji_html}</p>)
end
# rubocop:enable Layout/LineLength
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index ed49009d6d9..7fcd74cd37f 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -246,31 +246,41 @@ RSpec.describe Integration, feature_category: :integrations do
end
end
+ describe '#ci?' do
+ it 'is true when integration is a CI integration' do
+ expect(build(:jenkins_integration).ci?).to eq(true)
+ end
+
+ it 'is false when integration is not a ci integration' do
+ expect(build(:integration).ci?).to eq(false)
+ end
+ end
+
describe '.find_or_initialize_non_project_specific_integration' do
let!(:integration_1) { create(:jira_integration, project_id: nil, group_id: group.id) }
let!(:integration_2) { create(:jira_integration, project: project) }
it 'returns the right integration' do
- expect(Integration.find_or_initialize_non_project_specific_integration('jira', group_id: group))
+ expect(described_class.find_or_initialize_non_project_specific_integration('jira', group_id: group))
.to eq(integration_1)
end
it 'does not create a new integration' do
- expect { Integration.find_or_initialize_non_project_specific_integration('redmine', group_id: group) }
- .not_to change(Integration, :count)
+ expect { described_class.find_or_initialize_non_project_specific_integration('redmine', group_id: group) }
+ .not_to change(described_class, :count)
end
end
describe '.find_or_initialize_all_non_project_specific' do
shared_examples 'integration instances' do
it 'returns the available integration instances' do
- expect(Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).map(&:to_param))
- .to match_array(Integration.available_integration_names(include_project_specific: false))
+ expect(described_class.find_or_initialize_all_non_project_specific(described_class.for_instance).map(&:to_param))
+ .to match_array(described_class.available_integration_names(include_project_specific: false))
end
it 'does not create integration instances' do
- expect { Integration.find_or_initialize_all_non_project_specific(Integration.for_instance) }
- .not_to change(Integration, :count)
+ expect { described_class.find_or_initialize_all_non_project_specific(described_class.for_instance) }
+ .not_to change(described_class, :count)
end
end
@@ -282,19 +292,19 @@ RSpec.describe Integration, feature_category: :integrations do
end
before do
- attrs = Integration.available_integration_types(include_project_specific: false).map do
+ attrs = described_class.available_integration_types(include_project_specific: false).map do
integration_hash(_1)
end
- Integration.insert_all(attrs)
+ described_class.insert_all(attrs)
end
it_behaves_like 'integration instances'
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
before do
- Integration.insert(integration_hash(:mock_ci))
- Integration.delete_by(**integration_hash(:asana))
+ described_class.insert(integration_hash(:mock_ci))
+ described_class.delete_by(**integration_hash(:asana))
end
it_behaves_like 'integration instances'
@@ -1352,5 +1362,17 @@ RSpec.describe Integration, feature_category: :integrations do
async_execute
end
end
+
+ context 'when the Gitlab::SilentMode is enabled' do
+ before do
+ allow(Gitlab::SilentMode).to receive(:enabled?).and_return(true)
+ end
+
+ it 'does not queue a worker' do
+ expect(Integrations::ExecuteWorker).not_to receive(:perform_async)
+
+ async_execute
+ 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 f3346acae0a..9864fe38d3f 100644
--- a/spec/models/integrations/apple_app_store_spec.rb
+++ b/spec/models/integrations/apple_app_store_spec.rb
@@ -13,8 +13,7 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
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(true).for(:app_store_protected_refs) }
- it { is_expected.to allow_value(false).for(:app_store_protected_refs) }
+ it { is_expected.to allow_value(true, false).for(:app_store_protected_refs) }
it { is_expected.not_to allow_value(nil).for(:app_store_protected_refs) }
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) }
diff --git a/spec/models/integrations/asana_spec.rb b/spec/models/integrations/asana_spec.rb
index 43e876a4f47..376aec1088e 100644
--- a/spec/models/integrations/asana_spec.rb
+++ b/spec/models/integrations/asana_spec.rb
@@ -2,20 +2,24 @@
require 'spec_helper'
-RSpec.describe Integrations::Asana do
+RSpec.describe Integrations::Asana, feature_category: :integrations do
describe 'Validations' do
- context 'active' do
+ context 'when active' do
before do
subject.active = true
end
it { is_expected.to validate_presence_of :api_key }
end
+
+ context 'when inactive' do
+ it { is_expected.not_to validate_presence_of :api_key }
+ end
end
- describe 'Execute' do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
+ describe '#execute' do
+ let_it_be(:user) { build(:user) }
+ let_it_be(:project) { build(:project) }
let(:gid) { "123456789ABCD" }
let(:asana_task) { double(::Asana::Resources::Task) }
diff --git a/spec/models/integrations/assembla_spec.rb b/spec/models/integrations/assembla_spec.rb
index e9f4274952d..28cda0a1e75 100644
--- a/spec/models/integrations/assembla_spec.rb
+++ b/spec/models/integrations/assembla_spec.rb
@@ -2,34 +2,49 @@
require 'spec_helper'
-RSpec.describe Integrations::Assembla do
+RSpec.describe Integrations::Assembla, feature_category: :integrations do
include StubRequests
it_behaves_like Integrations::ResetSecretFields do
let(:integration) { described_class.new }
end
- describe "Execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ describe 'Validations' do
+ context 'when active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :token }
+ end
+
+ context 'when inactive' do
+ it { is_expected.not_to validate_presence_of :token }
+ end
+ end
+
+ describe "#execute" do
+ let_it_be(:user) { build(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:assembla_integration) { described_class.new }
+ let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+ let(:api_url) { 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret' }
before do
- @assembla_integration = described_class.new
- allow(@assembla_integration).to receive_messages(
+ allow(assembla_integration).to receive_messages(
project_id: project.id,
project: project,
token: 'verySecret',
subdomain: 'project_name'
)
- @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
- @api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
- stub_full_request(@api_url, method: :post)
+ stub_full_request(api_url, method: :post)
end
it "calls Assembla API" do
- @assembla_integration.execute(@sample_data)
- expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
- body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
+ assembla_integration.execute(sample_data)
+ expect(WebMock).to have_requested(:post, stubbed_hostname(api_url)).with(
+ body: /#{sample_data[:before]}.*#{sample_data[:after]}.*#{project.path}/
).once
end
end
diff --git a/spec/models/integrations/bamboo_spec.rb b/spec/models/integrations/bamboo_spec.rb
index 1d2c90dad51..3b459ab9d5b 100644
--- a/spec/models/integrations/bamboo_spec.rb
+++ b/spec/models/integrations/bamboo_spec.rb
@@ -2,26 +2,15 @@
require 'spec_helper'
-RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching, feature_category: :integrations do
include ReactiveCachingHelpers
include StubRequests
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
- let_it_be(:project) { create(:project) }
-
- subject(:integration) do
- described_class.create!(
- active: true,
- project: project,
- properties: {
- bamboo_url: bamboo_url,
- username: 'mic',
- password: 'password',
- build_key: 'foo'
- }
- )
- end
+ let_it_be(:project) { build(:project) }
+
+ subject(:integration) { build(:bamboo_integration, project: project, bamboo_url: bamboo_url) }
it_behaves_like Integrations::BaseCi
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 13dd9e03ab1..675035095c5 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
it { expect(subject.category).to eq(:chat) }
end
- describe 'validations' do
+ describe 'Validations' do
before do
subject.active = active
@@ -112,9 +112,9 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
end
context 'when the data object has a label' do
- let_it_be(:label) { create(:label, project: project, name: 'Bug') }
- let_it_be(:label_2) { create(:label, project: project, name: 'Community contribution') }
- let_it_be(:label_3) { create(:label, project: project, name: 'Backend') }
+ let_it_be(:label) { build(:label, project: project, name: 'Bug') }
+ let_it_be(:label_2) { build(:label, project: project, name: 'Community contribution') }
+ let_it_be(:label_3) { build(:label, project: project, name: 'Backend') }
let_it_be(:issue) { create(:labeled_issue, project: project, labels: [label, label_2, label_3]) }
let_it_be(:note) { create(:note, noteable: issue, project: project) }
diff --git a/spec/models/integrations/base_issue_tracker_spec.rb b/spec/models/integrations/base_issue_tracker_spec.rb
index e1a764cd7cb..1bb24876222 100644
--- a/spec/models/integrations/base_issue_tracker_spec.rb
+++ b/spec/models/integrations/base_issue_tracker_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe Integrations::BaseIssueTracker do
- let(:integration) { Integrations::Redmine.new(project: project, active: true, issue_tracker_data: build(:issue_tracker_data)) }
+RSpec.describe Integrations::BaseIssueTracker, feature_category: :integrations do
+ let(:integration) { build(:redmine_integration, project: project, active: true, issue_tracker_data: build(:issue_tracker_data)) }
- let_it_be_with_refind(:project) { create :project }
+ let_it_be_with_refind(:project) { create(:project) }
describe 'default values' do
it { expect(subject.category).to eq(:issue_tracker) }
diff --git a/spec/models/integrations/base_slack_notification_spec.rb b/spec/models/integrations/base_slack_notification_spec.rb
index 8f7f4e8858d..ab977ca8fcc 100644
--- a/spec/models/integrations/base_slack_notification_spec.rb
+++ b/spec/models/integrations/base_slack_notification_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::BaseSlackNotification do
+RSpec.describe Integrations::BaseSlackNotification, feature_category: :integrations do
# This spec should only contain tests that cannot be tested through
# `base_slack_notification_shared_examples.rb`.
diff --git a/spec/models/integrations/base_third_party_wiki_spec.rb b/spec/models/integrations/base_third_party_wiki_spec.rb
index dbead636cb9..763f7131b94 100644
--- a/spec/models/integrations/base_third_party_wiki_spec.rb
+++ b/spec/models/integrations/base_third_party_wiki_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::BaseThirdPartyWiki do
+RSpec.describe Integrations::BaseThirdPartyWiki, feature_category: :integrations do
describe 'default values' do
it { expect(subject.category).to eq(:third_party_wiki) }
end
@@ -11,7 +11,7 @@ RSpec.describe Integrations::BaseThirdPartyWiki do
let_it_be_with_reload(:project) { create(:project) }
describe 'only one third party wiki per project' do
- subject(:integration) { create(:shimo_integration, project: project, active: true) }
+ subject(:integration) { build(:shimo_integration, project: project, active: true) }
before_all do
create(:confluence_integration, project: project, active: true)
@@ -35,7 +35,7 @@ RSpec.describe Integrations::BaseThirdPartyWiki do
end
context 'when integration is not on the project level' do
- subject(:integration) { create(:shimo_integration, :instance, active: true) }
+ subject(:integration) { build(:shimo_integration, :instance, active: true) }
it 'executes the validation' do
expect(integration.valid?(:manual_change)).to be_truthy
diff --git a/spec/models/integrations/bugzilla_spec.rb b/spec/models/integrations/bugzilla_spec.rb
index f05bc26d066..cef58589231 100644
--- a/spec/models/integrations/bugzilla_spec.rb
+++ b/spec/models/integrations/bugzilla_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Bugzilla do
+RSpec.describe Integrations::Bugzilla, feature_category: :integrations do
describe 'Validations' do
context 'when integration is active' do
before do
diff --git a/spec/models/integrations/buildkite_spec.rb b/spec/models/integrations/buildkite_spec.rb
index 29c649af6c6..f3231d50eae 100644
--- a/spec/models/integrations/buildkite_spec.rb
+++ b/spec/models/integrations/buildkite_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching, feature_category: :integrations do
include ReactiveCachingHelpers
include StubRequests
@@ -71,9 +71,7 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
describe '#hook_url' do
it 'returns the webhook url' do
- expect(integration.hook_url).to eq(
- 'https://webhook.buildkite.com/deliver/{webhook_token}'
- )
+ expect(integration.hook_url).to eq('https://webhook.buildkite.com/deliver/{webhook_token}')
end
end
@@ -103,9 +101,7 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
describe '#calculate_reactive_cache' do
describe '#commit_status' do
- let(:buildkite_full_url) do
- 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
- end
+ let(:buildkite_full_url) { 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123' }
subject { integration.calculate_reactive_cache('123', 'unused')[:commit_status] }
diff --git a/spec/models/integrations/chat_message/group_mention_message_spec.rb b/spec/models/integrations/chat_message/group_mention_message_spec.rb
new file mode 100644
index 00000000000..6fa486f3dc3
--- /dev/null
+++ b/spec/models/integrations/chat_message/group_mention_message_spec.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::GroupMentionMessage, feature_category: :integrations do
+ subject { described_class.new(args) }
+
+ let(:color) { '#345' }
+ let(:args) do
+ {
+ object_kind: 'group_mention',
+ mentioned: {
+ object_kind: 'group',
+ name: 'test/group',
+ url: 'http://test/group'
+ },
+ user: {
+ name: 'Test User',
+ username: 'test.user',
+ avatar_url: 'http://avatar'
+ },
+ project_name: 'Test Project',
+ project_url: 'http://project'
+ }
+ end
+
+ context 'for issue descriptions' do
+ let(:attachments) { [{ text: "Issue\ndescription\n123", color: color }] }
+
+ before do
+ args[:object_attributes] = {
+ object_kind: 'issue',
+ iid: '42',
+ title: 'Test Issue',
+ description: "Issue\ndescription\n123",
+ url: 'http://issue'
+ }
+ end
+
+ it 'returns the appropriate message' do
+ expect(subject.pretext).to eq(
+ 'Group <http://test/group|test/group> was mentioned ' \
+ 'in <http://issue|issue #42> ' \
+ 'of <http://project|Test Project>: ' \
+ '*Test Issue*'
+ )
+ expect(subject.attachments).to eq(attachments)
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns the appropriate message' do
+ expect(subject.pretext).to eq(
+ 'Group [test/group](http://test/group) was mentioned ' \
+ 'in [issue #42](http://issue) ' \
+ 'of [Test Project](http://project): ' \
+ '*Test Issue*'
+ )
+ expect(subject.attachments).to eq("Issue\ndescription\n123")
+ expect(subject.activity).to eq(
+ {
+ title: 'Group [test/group](http://test/group) was mentioned in [issue #42](http://issue)',
+ subtitle: 'of [Test Project](http://project)',
+ text: 'Test Issue',
+ image: 'http://avatar'
+ }
+ )
+ end
+ end
+ end
+
+ context 'for merge request descriptions' do
+ let(:attachments) { [{ text: "MR\ndescription\n123", color: color }] }
+
+ before do
+ args[:object_attributes] = {
+ object_kind: 'merge_request',
+ iid: '42',
+ title: 'Test MR',
+ description: "MR\ndescription\n123",
+ url: 'http://merge_request'
+ }
+ end
+
+ it 'returns the appropriate message' do
+ expect(subject.pretext).to eq(
+ 'Group <http://test/group|test/group> was mentioned ' \
+ 'in <http://merge_request|merge request !42> ' \
+ 'of <http://project|Test Project>: ' \
+ '*Test MR*'
+ )
+ expect(subject.attachments).to eq(attachments)
+ end
+ end
+
+ context 'for notes' do
+ let(:attachments) { [{ text: 'Test Comment', color: color }] }
+
+ before do
+ args[:object_attributes] = {
+ object_kind: 'note',
+ note: 'Test Comment',
+ url: 'http://note'
+ }
+ end
+
+ context 'on commits' do
+ before do
+ args[:commit] = {
+ id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
+ title: 'Test Commit',
+ message: "Commit\nmessage\n123\n"
+ }
+ end
+
+ it 'returns the appropriate message' do
+ expect(subject.pretext).to eq(
+ 'Group <http://test/group|test/group> was mentioned ' \
+ 'in <http://note|commit 5f163b2b> ' \
+ 'of <http://project|Test Project>: ' \
+ '*Test Commit*'
+ )
+ expect(subject.attachments).to eq(attachments)
+ end
+ end
+
+ context 'on issues' do
+ before do
+ args[:issue] = {
+ iid: '42',
+ title: 'Test Issue'
+ }
+ end
+
+ it 'returns the appropriate message' do
+ expect(subject.pretext).to eq(
+ 'Group <http://test/group|test/group> was mentioned ' \
+ 'in <http://note|issue #42> ' \
+ 'of <http://project|Test Project>: ' \
+ '*Test Issue*'
+ )
+ expect(subject.attachments).to eq(attachments)
+ end
+ end
+
+ context 'on merge requests' do
+ before do
+ args[:merge_request] = {
+ iid: '42',
+ title: 'Test MR'
+ }
+ end
+
+ it 'returns the appropriate message' do
+ expect(subject.pretext).to eq(
+ 'Group <http://test/group|test/group> was mentioned ' \
+ 'in <http://note|merge request !42> ' \
+ 'of <http://project|Test Project>: ' \
+ '*Test MR*'
+ )
+ expect(subject.attachments).to eq(attachments)
+ end
+ end
+ end
+
+ context 'for unsupported object types' do
+ before do
+ args[:object_attributes] = { object_kind: 'unsupported' }
+ end
+
+ it 'raises an error' do
+ expect { described_class.new(args) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context 'for notes on unsupported object types' do
+ before do
+ args[:object_attributes] = {
+ object_kind: 'note',
+ note: 'Test Comment',
+ url: 'http://note'
+ }
+ # Not adding a supported object type's attributes
+ end
+
+ it 'raises an error' do
+ expect { described_class.new(args) }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/models/integrations/confluence_spec.rb b/spec/models/integrations/confluence_spec.rb
index 999a532527d..d267e4a71c2 100644
--- a/spec/models/integrations/confluence_spec.rb
+++ b/spec/models/integrations/confluence_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Confluence do
+RSpec.describe Integrations::Confluence, feature_category: :integrations do
let_it_be(:project) { create(:project) }
describe 'Validations' do
@@ -49,10 +49,11 @@ RSpec.describe Integrations::Confluence do
end
context 'when the project wiki is not enabled' do
- it 'returns nil when both active or inactive', :aggregate_failures do
- project = create(:project, :wiki_disabled)
- subject.project = project
+ before do
+ allow(project).to receive(:wiki_enabled?).and_return(false)
+ end
+ it 'returns nil when both active or inactive', :aggregate_failures do
[true, false].each do |active|
subject.active = active
diff --git a/spec/models/integrations/custom_issue_tracker_spec.rb b/spec/models/integrations/custom_issue_tracker_spec.rb
index 11f98b99bbe..4be95a25a43 100644
--- a/spec/models/integrations/custom_issue_tracker_spec.rb
+++ b/spec/models/integrations/custom_issue_tracker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::CustomIssueTracker do
+RSpec.describe Integrations::CustomIssueTracker, feature_category: :integrations do
describe 'Validations' do
context 'when integration is active' do
before do
diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb
index c46face9702..2cb86fb680a 100644
--- a/spec/models/integrations/drone_ci_spec.rb
+++ b/spec/models/integrations/drone_ci_spec.rb
@@ -184,7 +184,7 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
"success" => "success"
}.each do |drone_status, our_status|
it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do
- stub_request(body: %Q({"status":"#{drone_status}"}))
+ stub_request(body: %({"status":"#{drone_status}"}))
is_expected.to eq(our_status)
end
diff --git a/spec/models/integrations/microsoft_teams_spec.rb b/spec/models/integrations/microsoft_teams_spec.rb
index f1d9071d232..4f5b4daad42 100644
--- a/spec/models/integrations/microsoft_teams_spec.rb
+++ b/spec/models/integrations/microsoft_teams_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::MicrosoftTeams do
+RSpec.describe Integrations::MicrosoftTeams, feature_category: :integrations do
it_behaves_like "chat integration", "Microsoft Teams" do
let(:client) { ::MicrosoftTeams::Notifier }
let(:client_arguments) { webhook_url }
diff --git a/spec/models/integrations/prometheus_spec.rb b/spec/models/integrations/prometheus_spec.rb
index 8aa9b12c4f0..da43d851b31 100644
--- a/spec/models/integrations/prometheus_spec.rb
+++ b/spec/models/integrations/prometheus_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'googleauth'
-RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, :snowplow do
+RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, :snowplow, feature_category: :metrics do
include PrometheusHelpers
include ReactiveCachingHelpers
@@ -37,8 +37,8 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
integration.manual_configuration = true
end
- it 'validates presence of api_url' do
- expect(integration).to validate_presence_of(:api_url)
+ it 'does not validates presence of api_url' do
+ expect(integration).not_to validate_presence_of(:api_url)
end
end
@@ -119,7 +119,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
context 'when configuration is not valid' do
before do
- integration.api_url = nil
+ integration.manual_configuration = nil
end
it 'returns failure message' do
diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb
index e32088a2f79..c4c7202fae0 100644
--- a/spec/models/integrations/teamcity_spec.rb
+++ b/spec/models/integrations/teamcity_spec.rb
@@ -307,7 +307,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
def stub_post_to_build_queue(branch:)
teamcity_full_url = "#{teamcity_url}/httpAuth/app/rest/buildQueue"
- body ||= %Q(<build branchName=\"#{branch}\"><buildType id=\"foo\"/></build>)
+ body ||= %(<build branchName=\"#{branch}\"><buildType id=\"foo\"/></build>)
auth = %w(mic password)
stub_full_request(teamcity_full_url, method: :post).with(
@@ -322,7 +322,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
def stub_request(status: 200, body: nil, build_status: 'success')
auth = %w(mic password)
- body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
+ body ||= %({"build":{"status":"#{build_status}","id":"666"}})
stub_full_request(teamcity_full_url).with(basic_auth: auth).to_return(
status: status,
diff --git a/spec/models/integrations/unify_circuit_spec.rb b/spec/models/integrations/unify_circuit_spec.rb
index 7a91b2d3c11..017443c799f 100644
--- a/spec/models/integrations/unify_circuit_spec.rb
+++ b/spec/models/integrations/unify_circuit_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Integrations::UnifyCircuit do
+RSpec.describe Integrations::UnifyCircuit, feature_category: :integrations do
it_behaves_like "chat integration", "Unify Circuit" do
let(:client_arguments) { webhook_url }
let(:payload) do
diff --git a/spec/models/integrations/webex_teams_spec.rb b/spec/models/integrations/webex_teams_spec.rb
index b5cba6762aa..50a1383292b 100644
--- a/spec/models/integrations/webex_teams_spec.rb
+++ b/spec/models/integrations/webex_teams_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Integrations::WebexTeams do
+RSpec.describe Integrations::WebexTeams, feature_category: :integrations do
it_behaves_like "chat integration", "Webex Teams" do
let(:client_arguments) { webhook_url }
let(:payload) do
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 59ade8783e5..0d4756659d6 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe InternalId do
it 'increments counter with in_transaction: "false"' do
allow(ApplicationRecord.connection).to receive(:transaction_open?) { false }
- expect(InternalId.internal_id_transactions_total).to receive(:increment)
+ expect(described_class.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original
subject
@@ -98,7 +98,7 @@ RSpec.describe InternalId do
context 'when executed within transaction' do
it 'increments counter with in_transaction: "true"' do
- expect(InternalId.internal_id_transactions_total).to receive(:increment)
+ expect(described_class.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
@@ -148,7 +148,7 @@ RSpec.describe InternalId do
it 'increments counter with in_transaction: "false"' do
allow(ApplicationRecord.connection).to receive(:transaction_open?) { false }
- expect(InternalId.internal_id_transactions_total).to receive(:increment)
+ expect(described_class.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original
subject
@@ -159,7 +159,7 @@ RSpec.describe InternalId do
let(:value) { 2 }
it 'increments counter with in_transaction: "true"' do
- expect(InternalId.internal_id_transactions_total).to receive(:increment)
+ expect(described_class.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
@@ -219,7 +219,7 @@ RSpec.describe InternalId do
it 'increments counter with in_transaction: "false"' do
allow(ApplicationRecord.connection).to receive(:transaction_open?) { false }
- expect(InternalId.internal_id_transactions_total).to receive(:increment)
+ expect(described_class.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original
subject
@@ -228,7 +228,7 @@ RSpec.describe InternalId do
context 'when executed within transaction' do
it 'increments counter with in_transaction: "true"' do
- expect(InternalId.internal_id_transactions_total).to receive(:increment)
+ expect(described_class.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
diff --git a/spec/models/issue_assignee_spec.rb b/spec/models/issue_assignee_spec.rb
index df8e91cd133..64afed37714 100644
--- a/spec/models/issue_assignee_spec.rb
+++ b/spec/models/issue_assignee_spec.rb
@@ -27,9 +27,9 @@ RSpec.describe IssueAssignee do
context 'in_projects' do
it 'returns issue assignees for given project' do
- expect(IssueAssignee.count).to eq 2
+ expect(described_class.count).to eq 2
- assignees = IssueAssignee.in_projects([project])
+ assignees = described_class.in_projects([project])
expect(assignees.count).to eq 1
expect(assignees.first.user_id).to eq project_issue.issue_assignees.first.user_id
@@ -39,9 +39,9 @@ RSpec.describe IssueAssignee do
context 'on_issues' do
it 'returns issue assignees for given issues' do
- expect(IssueAssignee.count).to eq 2
+ expect(described_class.count).to eq 2
- assignees = IssueAssignee.on_issues([project_issue])
+ assignees = described_class.on_issues([project_issue])
expect(assignees.count).to eq 1
expect(assignees.first.issue_id).to eq project_issue.issue_assignees.first.issue_id
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index ee47f90fb40..8d25ac93263 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -127,22 +127,6 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
- describe 'issue_type' do
- let(:issue) { build(:issue, issue_type: issue_type) }
-
- context 'when a valid type' do
- let(:issue_type) { :issue }
-
- it { is_expected.to eq(true) }
- end
-
- context 'empty type' do
- let(:issue_type) { nil }
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#allowed_work_item_type_change' do
where(:old_type, :new_type, :is_valid) do
:issue | :incident | true
@@ -161,7 +145,7 @@ RSpec.describe Issue, feature_category: :team_planning do
it 'is possible to change type only between selected types' do
issue = create(:issue, old_type, project: reusable_project)
- issue.assign_attributes(work_item_type: WorkItems::Type.default_by_type(new_type), issue_type: new_type)
+ issue.assign_attributes(work_item_type: WorkItems::Type.default_by_type(new_type))
expect(issue.valid?).to eq(is_valid)
end
@@ -177,7 +161,7 @@ RSpec.describe Issue, feature_category: :team_planning do
let_it_be(:link) { create(:parent_link, work_item: child, work_item_parent: parent) }
it 'does not allow to make child not-confidential' do
- issue = Issue.find(child.id)
+ issue = described_class.find(child.id)
issue.confidential = false
expect(issue).not_to be_valid
@@ -186,7 +170,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
it 'allows to make parent not-confidential' do
- issue = Issue.find(parent.id)
+ issue = described_class.find(parent.id)
issue.confidential = false
expect(issue).to be_valid
@@ -199,7 +183,7 @@ RSpec.describe Issue, feature_category: :team_planning do
let_it_be(:link) { create(:parent_link, work_item: child, work_item_parent: parent) }
it 'does not allow to make parent confidential' do
- issue = Issue.find(parent.id)
+ issue = described_class.find(parent.id)
issue.confidential = true
expect(issue).not_to be_valid
@@ -208,7 +192,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
it 'allows to make child confidential' do
- issue = Issue.find(child.id)
+ issue = described_class.find(child.id)
issue.confidential = true
expect(issue).to be_valid
@@ -272,7 +256,7 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(issue.work_item_type_id).to eq(issue_type.id)
expect(WorkItems::Type).not_to receive(:default_by_type)
- issue.update!(work_item_type: incident_type, issue_type: :incident)
+ issue.update!(work_item_type: incident_type)
expect(issue.work_item_type_id).to eq(incident_type.id)
end
@@ -301,36 +285,13 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(issue.work_item_type_id).to be_nil
expect(WorkItems::Type).not_to receive(:default_by_type)
- issue.update!(work_item_type: incident_type, issue_type: :incident)
+ issue.update!(work_item_type: incident_type)
expect(issue.work_item_type_id).to eq(incident_type.id)
end
end
end
- describe '#check_issue_type_in_sync' do
- it 'raises an error if issue_type is out of sync' do
- issue = build(:issue, issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:task))
-
- expect do
- issue.save!
- end.to raise_error(Issue::IssueTypeOutOfSyncError)
- end
-
- it 'uses attributes to compare both issue_type values' do
- issue_type = WorkItems::Type.default_by_type(:issue)
- issue = build(:issue, issue_type: :issue, work_item_type: issue_type)
-
- attributes = double(:attributes)
- allow(issue).to receive(:attributes).and_return(attributes)
-
- expect(attributes).to receive(:[]).with('issue_type').twice.and_return('issue')
- expect(issue_type).to receive(:base_type).and_call_original
-
- issue.save!
- end
- end
-
describe '#record_create_action' do
it 'records the creation action after saving' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_created_action)
@@ -338,11 +299,13 @@ RSpec.describe Issue, feature_category: :team_planning do
create(:issue)
end
- it_behaves_like 'issue_edit snowplow tracking' do
+ it_behaves_like 'internal event tracking' do
let(:issue) { create(:issue) }
let(:project) { issue.project }
- let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CREATED }
let(:user) { issue.author }
+ let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CREATED }
+ let(:namespace) { project.namespace }
+
subject(:service_action) { issue }
end
end
@@ -465,18 +428,6 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(described_class.with_issue_type([]).to_sql).to include('WHERE 1=0')
end
end
-
- context 'when the issue_type_uses_work_item_types_table feature flag is disabled' do
- before do
- stub_feature_flags(issue_type_uses_work_item_types_table: false)
- end
-
- it 'uses the issue_type column for filtering' do
- expect do
- described_class.with_issue_type(:issue).to_a
- end.to make_queries_matching(/"issues"\."issue_type" = 0/)
- end
- end
end
describe '.without_issue_type' do
@@ -504,18 +455,6 @@ RSpec.describe Issue, feature_category: :team_planning do
}x
)
end
-
- context 'when the issue_type_uses_work_item_types_table feature flag is disabled' do
- before do
- stub_feature_flags(issue_type_uses_work_item_types_table: false)
- end
-
- it 'uses the issue_type column for filtering' do
- expect do
- described_class.without_issue_type(:issue).to_a
- end.to make_queries_matching(/"issues"\."issue_type" != 0/)
- end
- end
end
describe '.order_severity' do
@@ -1532,52 +1471,58 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe '#publicly_visible?' do
- context 'using a public project' do
- let(:project) { create(:project, :public) }
+ let(:project) { build(:project, project_visiblity) }
+ let(:issue) { build(:issue, confidential: confidential, project: project) }
- it 'returns true for a regular issue' do
- issue = build(:issue, project: project)
+ subject { issue.send(:publicly_visible?) }
- expect(issue).to be_truthy
- end
-
- it 'returns false for a confidential issue' do
- issue = build(:issue, :confidential, project: project)
+ where(:project_visiblity, :confidential, :expected_value) do
+ :public | false | true
+ :public | true | false
+ :internal | false | false
+ :internal | true | false
+ :private | false | false
+ :private | true | false
+ end
- expect(issue).not_to be_falsy
- end
+ with_them do
+ it { is_expected.to eq(expected_value) }
end
+ end
- context 'using an internal project' do
- let(:project) { create(:project, :internal) }
+ describe '#allow_possible_spam?' do
+ let_it_be(:issue) { build(:issue) }
- it 'returns false for a regular issue' do
- issue = build(:issue, project: project)
+ subject { issue.allow_possible_spam?(issue.author) }
- expect(issue).not_to be_falsy
- end
+ context 'when the `allow_possible_spam` application setting is turned off' do
+ context 'when the issue is private' do
+ it { is_expected.to eq(true) }
- it 'returns false for a confidential issue' do
- issue = build(:issue, :confidential, project: project)
+ context 'when the user is the support bot' do
+ before do
+ allow(issue.author).to receive(:support_bot?).and_return(true)
+ end
- expect(issue).not_to be_falsy
+ it { is_expected.to eq(false) }
+ end
end
- end
-
- context 'using a private project' do
- let(:project) { create(:project, :private) }
- it 'returns false for a regular issue' do
- issue = build(:issue, project: project)
+ context 'when the issue is public' do
+ before do
+ allow(issue).to receive(:publicly_visible?).and_return(true)
+ end
- expect(issue).not_to be_falsy
+ it { is_expected.to eq(false) }
end
+ end
- it 'returns false for a confidential issue' do
- issue = build(:issue, :confidential, project: project)
-
- expect(issue).not_to be_falsy
+ context 'when the `allow_possible_spam` application setting is turned on' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
end
+
+ it { is_expected.to eq(true) }
end
end
@@ -1590,24 +1535,24 @@ RSpec.describe Issue, feature_category: :team_planning do
false | Gitlab::VisibilityLevel::PUBLIC | false | { description: 'new' } | true
false | Gitlab::VisibilityLevel::PUBLIC | false | { title: 'new' } | true
# confidential to non-confidential
- false | Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | true
+ false | Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | false
# non-confidential to confidential
false | Gitlab::VisibilityLevel::PUBLIC | false | { confidential: true } | false
# spammable attributes changing on confidential
- false | Gitlab::VisibilityLevel::PUBLIC | true | { description: 'new' } | false
+ false | Gitlab::VisibilityLevel::PUBLIC | true | { description: 'new' } | true
# spammable attributes changing while changing to confidential
- false | Gitlab::VisibilityLevel::PUBLIC | false | { title: 'new', confidential: true } | false
+ false | Gitlab::VisibilityLevel::PUBLIC | false | { title: 'new', confidential: true } | true
# spammable attribute not changing
false | Gitlab::VisibilityLevel::PUBLIC | false | { description: 'original description' } | false
# non-spammable attribute changing
false | Gitlab::VisibilityLevel::PUBLIC | false | { weight: 3 } | false
# spammable attributes changing on non-public
- false | Gitlab::VisibilityLevel::INTERNAL | false | { description: 'new' } | false
- false | Gitlab::VisibilityLevel::PRIVATE | false | { description: 'new' } | false
+ false | Gitlab::VisibilityLevel::INTERNAL | false | { description: 'new' } | true
+ false | Gitlab::VisibilityLevel::PRIVATE | false | { description: 'new' } | true
### support-bot cases
# confidential to non-confidential
- true | Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | true
+ true | Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | false
# non-confidential to confidential
true | Gitlab::VisibilityLevel::PUBLIC | false | { confidential: true } | false
# spammable attributes changing on confidential
@@ -1877,39 +1822,17 @@ RSpec.describe Issue, feature_category: :team_planning do
describe '#issue_type' do
let_it_be(:issue) { create(:issue) }
- context 'when the issue_type_uses_work_item_types_table feature flag is enabled' do
- it 'gets the type field from the work_item_types table' do
- expect(issue).to receive_message_chain(:work_item_type, :base_type)
-
- issue.issue_type
- end
+ it 'gets the type field from the work_item_types table' do
+ expect(issue).to receive_message_chain(:work_item_type, :base_type)
- context 'when the issue is not persisted' do
- it 'uses the default work item type' do
- non_persisted_issue = build(:issue, work_item_type: nil)
-
- expect(non_persisted_issue.issue_type).to eq(described_class::DEFAULT_ISSUE_TYPE.to_s)
- end
- end
+ issue.issue_type
end
- context 'when the issue_type_uses_work_item_types_table feature flag is disabled' do
- before do
- stub_feature_flags(issue_type_uses_work_item_types_table: false)
- end
-
- it 'does not get the value from the work_item_types table' do
- expect(issue).not_to receive(:work_item_type)
+ context 'when the issue is not persisted' do
+ it 'uses the default work item type' do
+ non_persisted_issue = build(:issue, work_item_type: nil)
- issue.issue_type
- end
-
- context 'when the issue is not persisted' do
- it 'uses the default work item type' do
- non_persisted_issue = build(:issue, work_item_type: nil)
-
- expect(non_persisted_issue.issue_type).to eq(described_class::DEFAULT_ISSUE_TYPE.to_s)
- end
+ expect(non_persisted_issue.issue_type).to eq(described_class::DEFAULT_ISSUE_TYPE.to_s)
end
end
end
@@ -1944,7 +1867,7 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
before do
- issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
+ issue.update!(work_item_type: WorkItems::Type.default_by_type(issue_type))
end
specify do
@@ -1964,7 +1887,7 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
before do
- issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
+ issue.update!(work_item_type: WorkItems::Type.default_by_type(issue_type))
end
specify do
@@ -2080,7 +2003,7 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe '#work_item_type_with_default' do
- subject { Issue.new.work_item_type_with_default }
+ subject { described_class.new.work_item_type_with_default }
it { is_expected.to eq(WorkItems::Type.default_by_type(::Issue::DEFAULT_ISSUE_TYPE)) }
end
@@ -2108,51 +2031,4 @@ RSpec.describe Issue, feature_category: :team_planning do
expect { issue1.unsubscribe_email_participant(email) }.not_to change { issue2.issue_email_participants.count }
end
end
-
- describe 'issue_type enum generated methods' do
- describe '#<issue_type>?' do
- let_it_be(:issue) { create(:issue, project: reusable_project) }
-
- where(issue_type: WorkItems::Type.base_types.keys)
-
- with_them do
- it 'raises an error if called' do
- expect { issue.public_send("#{issue_type}?".to_sym) }.to raise_error(
- Issue::ForbiddenColumnUsed,
- a_string_matching(/`issue\.#{issue_type}\?` uses the `issue_type` column underneath/)
- )
- end
- end
- end
-
- describe '.<issue_type> scopes' do
- where(issue_type: WorkItems::Type.base_types.keys)
-
- with_them do
- it 'raises an error if called' do
- expect { Issue.public_send(issue_type.to_sym) }.to raise_error(
- Issue::ForbiddenColumnUsed,
- a_string_matching(/`Issue\.#{issue_type}` uses the `issue_type` column underneath/)
- )
- end
-
- context 'when called in a production environment' do
- before do
- stub_rails_env('production')
- end
-
- it 'returns issues scoped by type instead of raising an error' do
- issue = create(
- :issue,
- issue_type: issue_type,
- work_item_type: WorkItems::Type.default_by_type(issue_type),
- project: reusable_project
- )
-
- expect(Issue.public_send(issue_type.to_sym)).to contain_exactly(issue)
- end
- end
- end
- end
- end
end
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index c6bec215145..15931e18715 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe LabelLink do
it { is_expected.to belong_to(:label) }
it { is_expected.to belong_to(:target) }
- it_behaves_like 'a BulkInsertSafe model', LabelLink do
+ it_behaves_like 'a BulkInsertSafe model', described_class do
let(:valid_items_for_bulk_insertion) { build_list(:label_link, 10) }
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end
diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb
index 7378beeed06..c9b43a55ca7 100644
--- a/spec/models/lfs_objects_project_spec.rb
+++ b/spec/models/lfs_objects_project_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe LfsObjectsProject do
expect do
result = described_class.link_to_project!(subject.lfs_object, subject.project)
- expect(result).to be_a(LfsObjectsProject)
+ expect(result).to be_a(described_class)
end.not_to change { described_class.count }
end
diff --git a/spec/models/loose_foreign_keys/deleted_record_spec.rb b/spec/models/loose_foreign_keys/deleted_record_spec.rb
index 2513a9043ad..0c16a725663 100644
--- a/spec/models/loose_foreign_keys/deleted_record_spec.rb
+++ b/spec/models/loose_foreign_keys/deleted_record_spec.rb
@@ -146,7 +146,7 @@ RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
expect { described_class.create!(fully_qualified_table_name: table, primary_key_value: 5) }.not_to raise_error
# after processing old records
- LooseForeignKeys::DeletedRecord.for_partition(1).update_all(status: :processed)
+ described_class.for_partition(1).update_all(status: :processed)
partition_manager.sync_partitions
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index b242de48be0..d21edea9751 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -254,18 +254,18 @@ RSpec.describe Member, feature_category: :groups_and_projects do
]
end
- subject { Member.in_hierarchy(project) }
+ subject { described_class.in_hierarchy(project) }
it { is_expected.to contain_exactly(*hierarchy_members) }
context 'with scope prefix' do
- subject { Member.where.not(source: project).in_hierarchy(subgroup) }
+ subject { described_class.where.not(source: project).in_hierarchy(subgroup) }
it { is_expected.to contain_exactly(root_ancestor_member, subgroup_member, subgroup_project_member) }
end
context 'with scope suffix' do
- subject { Member.in_hierarchy(project).where.not(source: project) }
+ subject { described_class.in_hierarchy(project).where.not(source: project) }
it { is_expected.to contain_exactly(root_ancestor_member, subgroup_member, subgroup_project_member) }
end
@@ -668,6 +668,15 @@ RSpec.describe Member, feature_category: :groups_and_projects do
end
end
+ describe '.with_user' do
+ it 'returns the member' do
+ not_a_member = create(:user)
+
+ expect(described_class.with_user(@owner_user)).to eq([@owner])
+ expect(described_class.with_user(not_a_member)).to be_empty
+ end
+ end
+
describe '.active_state' do
let_it_be(:active_group_member) { create(:group_member, group: group) }
let_it_be(:active_project_member) { create(:project_member, project: project) }
@@ -859,7 +868,7 @@ RSpec.describe Member, feature_category: :groups_and_projects do
expect(member.invite_accepted_at).to be_nil
expect(member.invite_token).not_to be_nil
- expect_any_instance_of(Member).not_to receive(:after_accept_invite)
+ expect_any_instance_of(described_class).not_to receive(:after_accept_invite)
end
it 'schedules a TasksToBeDone::CreateWorker task' do
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index e197d83b621..a07829abece 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -31,15 +31,6 @@ RSpec.describe GroupMember, feature_category: :cell do
expect(described_class.of_ldap_type).to eq([group_member])
end
end
-
- describe '.with_user' do
- it 'returns requested user' do
- group_member = create(:group_member, user: user_2)
- create(:group_member, user: user_1)
-
- expect(described_class.with_user(user_2)).to eq([group_member])
- end
- end
end
describe 'delegations' do
diff --git a/spec/models/merge_request/diff_llm_summary_spec.rb b/spec/models/merge_request/diff_llm_summary_spec.rb
deleted file mode 100644
index 860457add62..00000000000
--- a/spec/models/merge_request/diff_llm_summary_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::MergeRequest::DiffLlmSummary, feature_category: :code_review_workflow do
- let_it_be_with_reload(:project) { create(:project, :repository) }
-
- subject(:merge_request_diff_llm_summary) { build(:merge_request_diff_llm_summary) }
-
- describe 'associations' do
- it { is_expected.to belong_to(:merge_request_diff) }
- it { is_expected.to belong_to(:user).optional }
- it { is_expected.to validate_uniqueness_of(:merge_request_diff_id) }
- it { is_expected.to validate_presence_of(:content) }
- it { is_expected.to validate_length_of(:content).is_at_most(2056) }
- it { is_expected.to validate_presence_of(:provider) }
- end
-end
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index b1c2a9b1111..e9e4956dc41 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe MergeRequest::Metrics do
end
end
- it_behaves_like 'database events tracking batch 2' do
+ it_behaves_like 'database events tracking', feature_category: :service_ping do
let(:merge_request) { create(:merge_request) }
let(:record) { merge_request.metrics }
diff --git a/spec/models/merge_request_assignee_spec.rb b/spec/models/merge_request_assignee_spec.rb
index 73bf7d02468..fd13027bcc4 100644
--- a/spec/models/merge_request_assignee_spec.rb
+++ b/spec/models/merge_request_assignee_spec.rb
@@ -28,9 +28,9 @@ RSpec.describe MergeRequestAssignee do
context 'in_projects' do
it 'returns issue assignees for given project' do
- expect(MergeRequestAssignee.count).to eq 2
+ expect(described_class.count).to eq 2
- assignees = MergeRequestAssignee.in_projects([project])
+ assignees = described_class.in_projects([project])
expect(assignees.count).to eq 1
expect(assignees.first.user_id).to eq project_merge_request.merge_request_assignees.first.user_id
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 78f9fb5b7d3..5cb96809970 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow d
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
- it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffCommit do
+ it_behaves_like 'a BulkInsertSafe model', described_class do
let(:valid_items_for_bulk_insertion) do
build_list(:merge_request_diff_commit, 10) do |mr_diff_commit|
mr_diff_commit.merge_request_diff = create(:merge_request_diff)
@@ -82,7 +82,7 @@ RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow d
described_class.create_bulk(diff.id, [commits.first])
- commit_row = MergeRequestDiffCommit
+ commit_row = described_class
.find_by(merge_request_diff_id: diff.id, relative_order: 0)
commit_user_row =
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
index eee7fe67ffb..1ad9de78c92 100644
--- a/spec/models/merge_request_diff_file_spec.rb
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe MergeRequestDiffFile, feature_category: :code_review_workflow do
- it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffFile do
+ it_behaves_like 'a BulkInsertSafe model', described_class do
let(:valid_items_for_bulk_insertion) do
build_list(:merge_request_diff_file, 10) do |mr_diff_file|
mr_diff_file.merge_request_diff = create(:merge_request_diff)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index e16f7a94eb7..bf71d289105 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -415,8 +415,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it 'does not create duplicated metrics records when MR is concurrently updated' do
merge_request.metrics.destroy!
- instance1 = MergeRequest.find(merge_request.id)
- instance2 = MergeRequest.find(merge_request.id)
+ instance1 = described_class.find(merge_request.id)
+ instance2 = described_class.find(merge_request.id)
instance1.ensure_metrics!
instance2.ensure_metrics!
@@ -3259,6 +3259,20 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
+ describe '#skipped_mergeable_checks' do
+ subject { build_stubbed(:merge_request).skipped_mergeable_checks(options) }
+
+ where(:options, :skip_ci_check) do
+ {} | false
+ { auto_merge_requested: false } | false
+ { auto_merge_requested: true } | true
+ end
+
+ with_them do
+ it { is_expected.to include(skip_ci_check: skip_ci_check) }
+ end
+ end
+
describe '#check_mergeability' do
let(:mergeability_service) { double }
@@ -5105,6 +5119,32 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
+ describe '#schedule_cleanup_refs' do
+ subject { merge_request.schedule_cleanup_refs(only: :train) }
+
+ let(:merge_request) { build(:merge_request, source_project: create(:project, :repository)) }
+
+ it 'does schedule MergeRequests::CleanupRefWorker' do
+ expect(MergeRequests::CleanupRefWorker).to receive(:perform_async).with(merge_request.id, 'train')
+
+ subject
+ end
+
+ context 'when merge_request_cleanup_ref_worker_async is disabled' do
+ before do
+ stub_feature_flags(merge_request_cleanup_ref_worker_async: false)
+ end
+
+ it 'deletes all refs from the target project' do
+ expect(merge_request.target_project.repository)
+ .to receive(:delete_refs)
+ .with(merge_request.train_ref_path)
+
+ subject
+ end
+ end
+ end
+
describe '#cleanup_refs' do
subject { merge_request.cleanup_refs(only: only) }
@@ -5272,7 +5312,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
environment: envs[2]
)
- merge_request_relation = MergeRequest.where(id: merge_request.id)
+ merge_request_relation = described_class.where(id: merge_request.id)
created.link_merge_requests(merge_request_relation)
success.link_merge_requests(merge_request_relation)
failed.link_merge_requests(merge_request_relation)
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 1c43eafb576..1f0f89fea60 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestone do
+RSpec.describe Milestone, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:group) { create(:group) }
@@ -146,11 +146,11 @@ RSpec.describe Milestone do
let_it_be(:milestone) { create(:milestone, project: project) }
it 'returns true for a predefined Milestone ID' do
- expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
+ expect(described_class.predefined_id?(described_class::Upcoming.id)).to be true
end
it 'returns false for a Milestone ID that is not predefined' do
- expect(Milestone.predefined_id?(milestone.id)).to be false
+ expect(described_class.predefined_id?(milestone.id)).to be false
end
end
@@ -732,4 +732,44 @@ RSpec.describe Milestone do
expect(milestone.lock_version).to be_present
end
end
+
+ describe '#check_for_spam?' do
+ let_it_be(:milestone) { build_stubbed(:milestone, project: project) }
+
+ subject { milestone.check_for_spam? }
+
+ context 'when spammable attribute title has changed' do
+ before do
+ milestone.title = 'New title'
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when spammable attribute description has changed' do
+ before do
+ milestone.description = 'New description'
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when spammable attribute has changed but parent is private' do
+ before do
+ milestone.title = 'New title'
+ milestone.parent.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when no spammable attribute has changed' do
+ before do
+ milestone.title = milestone.title_was
+ milestone.description = milestone.description_was
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index 9738a88b5b8..1ee35d6da03 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -14,6 +14,21 @@ RSpec.describe Ml::Experiment, feature_category: :mlops do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
it { is_expected.to have_many(:metadata) }
+ it { is_expected.to belong_to(:model).class_name('Ml::Model') }
+ end
+
+ describe '#destroy' do
+ it 'allow experiment without model to be destroyed' do
+ experiment = create(:ml_experiments, project: exp.project)
+
+ expect { experiment.destroy! }.to change { Ml::Experiment.count }.by(-1)
+ end
+
+ it 'throws error when destroying experiment with model' do
+ experiment = create(:ml_models, project: exp.project).default_experiment
+
+ expect { experiment.destroy! }.to raise_error(ActiveRecord::ActiveRecordError)
+ end
end
describe '.package_name' do
diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb
new file mode 100644
index 00000000000..397ea23dd85
--- /dev/null
+++ b/spec/models/ml/model_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::Model, feature_category: :mlops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_one(:default_experiment) }
+ it { is_expected.to have_many(:versions) }
+ end
+
+ describe '#valid?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:existing_model) { create(:ml_models, name: 'an_existing_model', project: project) }
+ let_it_be(:valid_name) { 'a_valid_name' }
+ let_it_be(:default_experiment) { create(:ml_experiments, name: valid_name, project: project) }
+
+ let(:name) { valid_name }
+
+ subject(:errors) do
+ m = described_class.new(name: name, project: project, default_experiment: default_experiment)
+ m.validate
+ m.errors
+ end
+
+ it 'validates a valid model version' do
+ expect(errors).to be_empty
+ end
+
+ describe 'name' do
+ where(:ctx, :name) do
+ 'name is blank' | ''
+ 'name is not valid package name' | '!!()()'
+ 'name is too large' | ('a' * 256)
+ 'name is not unique in the project' | 'an_existing_model'
+ end
+ with_them do
+ it { expect(errors).to include(:name) }
+ end
+ end
+
+ describe 'default_experiment' do
+ context 'when experiment name name is different than model name' do
+ before do
+ allow(default_experiment).to receive(:name).and_return("#{name}a")
+ end
+
+ it { expect(errors).to include(:default_experiment) }
+ end
+
+ context 'when model version project is different than model project' do
+ before do
+ allow(default_experiment).to receive(:project_id).and_return(project.id + 1)
+ end
+
+ it { expect(errors).to include(:default_experiment) }
+ end
+ end
+ end
+end
diff --git a/spec/models/ml/model_version_spec.rb b/spec/models/ml/model_version_spec.rb
new file mode 100644
index 00000000000..ef53a1ac3a0
--- /dev/null
+++ b/spec/models/ml/model_version_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelVersion, feature_category: :mlops do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:base_project) { create(:project) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:model) }
+ it { is_expected.to belong_to(:package) }
+ end
+
+ describe 'validation' do
+ let_it_be(:valid_version) { 'valid_version' }
+ let_it_be(:model) { create(:ml_models, project: base_project) }
+ let_it_be(:valid_package) do
+ build_stubbed(:ml_model_package, project: base_project, version: valid_version, name: model.name)
+ end
+
+ let(:package) { valid_package }
+ let(:version) { valid_version }
+
+ subject(:errors) do
+ mv = described_class.new(version: version, model: model, package: package, project: model.project)
+ mv.validate
+ mv.errors
+ end
+
+ it 'validates a valid model version' do
+ expect(errors).to be_empty
+ end
+
+ describe 'version' do
+ where(:ctx, :version) do
+ 'version is blank' | ''
+ 'version is not valid package version' | '!!()()'
+ 'version is too large' | ('a' * 256)
+ end
+ with_them do
+ it { expect(errors).to include(:version) }
+ end
+
+ context 'when version is not unique in project+name' do
+ let_it_be(:existing_model_version) do
+ create(:ml_model_versions, model: model)
+ end
+
+ let(:version) { existing_model_version.version }
+
+ it { expect(errors).to include(:version) }
+ end
+ end
+
+ describe 'model' do
+ context 'when project is different' do
+ before do
+ allow(model).to receive(:project_id).and_return(non_existing_record_id)
+ end
+
+ it { expect(errors[:model]).to include('model project must be the same') }
+ end
+ end
+
+ describe 'package' do
+ where(:property, :value, :error_message) do
+ :name | 'another_name' | 'package name must be the same'
+ :version | 'another_version' | 'package version must be the same'
+ :project_id | 0 | 'package project must be the same'
+ end
+ with_them do
+ before do
+ allow(package).to receive(property).and_return(:value)
+ end
+
+ it { expect(errors[:package]).to include(error_message) }
+ end
+
+ context 'when package is not ml_model' do
+ let(:package) do
+ build_stubbed(:generic_package, project: base_project, name: model.name, version: valid_version)
+ end
+
+ it { expect(errors[:package]).to include('package must be ml_model') }
+ end
+ end
+ end
+end
diff --git a/spec/models/namespace/package_setting_spec.rb b/spec/models/namespace/package_setting_spec.rb
index 72ecad42a70..9dfb58301b1 100644
--- a/spec/models/namespace/package_setting_spec.rb
+++ b/spec/models/namespace/package_setting_spec.rb
@@ -11,11 +11,9 @@ RSpec.describe Namespace::PackageSetting do
it { is_expected.to validate_presence_of(:namespace) }
describe '#maven_duplicates_allowed' do
- it { is_expected.to allow_value(true).for(:maven_duplicates_allowed) }
- it { is_expected.to allow_value(false).for(:maven_duplicates_allowed) }
+ it { is_expected.to allow_value(true, false).for(:maven_duplicates_allowed) }
it { is_expected.not_to allow_value(nil).for(:maven_duplicates_allowed) }
- it { is_expected.to allow_value(true).for(:generic_duplicates_allowed) }
- it { is_expected.to allow_value(false).for(:generic_duplicates_allowed) }
+ it { is_expected.to allow_value(true, false).for(:generic_duplicates_allowed) }
it { is_expected.not_to allow_value(nil).for(:generic_duplicates_allowed) }
end
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
index a7f21e3a07f..f2c661c1cfb 100644
--- a/spec/models/namespace/root_storage_statistics_spec.rb
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespace::RootStorageStatistics, type: :model do
+RSpec.describe Namespace::RootStorageStatistics, type: :model, feature_category: :consumables_cost_management do
it { is_expected.to belong_to :namespace }
it { is_expected.to have_one(:route).through(:namespace) }
@@ -123,10 +123,21 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let_it_be(:group1) { create(:group, parent: root_group) }
let_it_be(:subgroup1) { create(:group, parent: group1) }
let_it_be(:group2) { create(:group, parent: root_group) }
- let_it_be(:root_namespace_stat) { create(:namespace_statistics, namespace: root_group, storage_size: 100, dependency_proxy_size: 100) }
- let_it_be(:group1_namespace_stat) { create(:namespace_statistics, namespace: group1, storage_size: 200, dependency_proxy_size: 200) }
- let_it_be(:group2_namespace_stat) { create(:namespace_statistics, namespace: group2, storage_size: 300, dependency_proxy_size: 300) }
- let_it_be(:subgroup1_namespace_stat) { create(:namespace_statistics, namespace: subgroup1, storage_size: 300, dependency_proxy_size: 100) }
+ let_it_be(:root_namespace_stat) do
+ create(:namespace_statistics, namespace: root_group, storage_size: 100, dependency_proxy_size: 100)
+ end
+
+ let_it_be(:group1_namespace_stat) do
+ create(:namespace_statistics, namespace: group1, storage_size: 200, dependency_proxy_size: 200)
+ end
+
+ let_it_be(:group2_namespace_stat) do
+ create(:namespace_statistics, namespace: group2, storage_size: 300, dependency_proxy_size: 300)
+ end
+
+ let_it_be(:subgroup1_namespace_stat) do
+ create(:namespace_statistics, namespace: subgroup1, storage_size: 300, dependency_proxy_size: 100)
+ end
let(:namespace) { root_group }
@@ -148,8 +159,12 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
total_pipeline_artifacts_size = project_stat1.pipeline_artifacts_size + project_stat2.pipeline_artifacts_size
total_uploads_size = project_stat1.uploads_size + project_stat2.uploads_size
total_wiki_size = project_stat1.wiki_size + project_stat2.wiki_size
- total_dependency_proxy_size = root_namespace_stat.dependency_proxy_size + group1_namespace_stat.dependency_proxy_size + group2_namespace_stat.dependency_proxy_size + subgroup1_namespace_stat.dependency_proxy_size
- total_storage_size = project_stat1.storage_size + project_stat2.storage_size + root_namespace_stat.storage_size + group1_namespace_stat.storage_size + group2_namespace_stat.storage_size + subgroup1_namespace_stat.storage_size
+ total_dependency_proxy_size = root_namespace_stat.dependency_proxy_size +
+ group1_namespace_stat.dependency_proxy_size + group2_namespace_stat.dependency_proxy_size +
+ subgroup1_namespace_stat.dependency_proxy_size
+ total_storage_size = project_stat1.storage_size + project_stat2.storage_size +
+ root_namespace_stat.storage_size + group1_namespace_stat.storage_size +
+ group2_namespace_stat.storage_size + subgroup1_namespace_stat.storage_size
expect(root_storage_statistics.repository_size).to eq(total_repository_size)
expect(root_storage_statistics.lfs_objects_size).to eq(total_lfs_objects_size)
@@ -209,7 +224,8 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
root_storage_statistics.recalculate!
- expect(root_storage_statistics.snippets_size).to eq(total_personal_snippets_size + total_project_snippets_size)
+ total = total_personal_snippets_size + total_project_snippets_size
+ expect(root_storage_statistics.snippets_size).to eq(total)
end
context 'when personal snippets do not have statistics' do
@@ -219,7 +235,8 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
root_storage_statistics.recalculate!
- expect(root_storage_statistics.snippets_size).to eq(total_project_snippets_size + snippets.last.statistics.repository_size)
+ total = total_project_snippets_size + snippets.last.statistics.repository_size
+ expect(root_storage_statistics.snippets_size).to eq(total)
end
end
end
@@ -293,7 +310,9 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
root_storage_statistics.reload
expect(root_storage_statistics.private_forks_storage_size).to eq(project_fork.statistics.storage_size)
- expect(root_storage_statistics.storage_size).to eq(project.statistics.storage_size + project_fork.statistics.storage_size)
+
+ total = project.statistics.storage_size + project_fork.statistics.storage_size
+ expect(root_storage_statistics.storage_size).to eq(total)
end
it 'sets the public forks storage size back to zero' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 3d7d5062ca7..1c02b4754fa 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
let(:repository_storage) { 'default' }
describe 'associations' do
+ it { is_expected.to belong_to :organization }
it { is_expected.to have_many :projects }
it { is_expected.to have_many :project_statistics }
it { is_expected.to belong_to :parent }
@@ -375,7 +376,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
describe 'handling STI', :aggregate_failures do
let(:namespace_type) { nil }
let(:parent) { nil }
- let(:namespace) { Namespace.find(create(:namespace, type: namespace_type, parent: parent).id) }
+ let(:namespace) { described_class.find(create(:namespace, type: namespace_type, parent: parent).id) }
context 'creating a Group' do
let(:namespace_type) { group_sti_name }
@@ -392,7 +393,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
let(:parent) { create(:group) }
it 'is the correct type of namespace' do
- expect(Namespace.find(namespace.id)).to be_a(Namespaces::ProjectNamespace)
+ expect(described_class.find(namespace.id)).to be_a(Namespaces::ProjectNamespace)
expect(namespace.kind).to eq('project')
expect(namespace.project_namespace?).to be_truthy
end
@@ -402,7 +403,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
let(:namespace_type) { user_sti_name }
it 'is the correct type of namespace' do
- expect(Namespace.find(namespace.id)).to be_a(Namespaces::UserNamespace)
+ expect(described_class.find(namespace.id)).to be_a(Namespaces::UserNamespace)
expect(namespace.kind).to eq('user')
expect(namespace.user_namespace?).to be_truthy
end
@@ -421,7 +422,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
let(:namespace_type) { 'nonsense' }
it 'creates a default Namespace' do
- expect(Namespace.find(namespace.id)).to be_a(Namespace)
+ expect(described_class.find(namespace.id)).to be_a(described_class)
expect(namespace.kind).to eq('user')
expect(namespace.user_namespace?).to be_truthy
end
@@ -587,7 +588,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
it 'returns value that matches database' do
- expect(namespace.traversal_ids).to eq Namespace.find(namespace.id).traversal_ids
+ expect(namespace.traversal_ids).to eq described_class.find(namespace.id).traversal_ids
end
end
@@ -598,7 +599,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
it 'returns database value' do
- expect(namespace.traversal_ids).to eq Namespace.find(namespace.id).traversal_ids
+ expect(namespace.traversal_ids).to eq described_class.find(namespace.id).traversal_ids
end
end
@@ -684,14 +685,6 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
it_behaves_like 'makes recursive queries'
end
-
- context 'when feature flag :use_traversal_ids_for_descendants_scopes is disabled' do
- before do
- stub_feature_flags(use_traversal_ids_for_descendants_scopes: false)
- end
-
- it_behaves_like 'makes recursive queries'
- end
end
describe '.self_and_descendant_ids' do
@@ -708,14 +701,6 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
it_behaves_like 'makes recursive queries'
end
-
- context 'when feature flag :use_traversal_ids_for_descendants_scopes is disabled' do
- before do
- stub_feature_flags(use_traversal_ids_for_descendants_scopes: false)
- end
-
- it_behaves_like 'makes recursive queries'
- end
end
end
@@ -889,6 +874,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
context 'when Gitlab API is supported' do
before do
+ allow(Gitlab).to receive(:com_except_jh?).and_return(true)
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
@@ -931,7 +917,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects 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(Gitlab).to receive(:com_except_jh?).and_return(true)
allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(gitlab_api_supported)
allow(project_namespace).to receive_message_chain(:all_container_repositories, :empty?).and_return(no_container_repositories)
allow(project_namespace).to receive_message_chain(:all_container_repositories, :all_migrated?).and_return(all_migrated)
@@ -1142,8 +1128,9 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
project1
project2
statistics = described_class.with_statistics.find(namespace.id)
+ expected_storage_size = project1.statistics.storage_size + project2.statistics.storage_size
- expect(statistics.storage_size).to eq 3995
+ expect(statistics.storage_size).to eq expected_storage_size
expect(statistics.repository_size).to eq 111
expect(statistics.wiki_size).to eq 555
expect(statistics.lfs_objects_size).to eq 222
@@ -1606,96 +1593,6 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
- describe '#use_traversal_ids_for_ancestors?' do
- let_it_be(:namespace, reload: true) { create(:namespace) }
-
- subject { namespace.use_traversal_ids_for_ancestors? }
-
- context 'when use_traversal_ids_for_ancestors? feature flag is true' do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestors: true)
- end
-
- it { is_expected.to eq true }
-
- it_behaves_like 'disabled feature flag when traversal_ids is blank'
- end
-
- context 'when use_traversal_ids_for_ancestors? feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestors: false)
- end
-
- it { is_expected.to eq false }
- end
-
- context 'when use_traversal_ids? feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids: false)
- end
-
- it { is_expected.to eq false }
- end
- end
-
- describe '#use_traversal_ids_for_ancestors_upto?' do
- let_it_be(:namespace, reload: true) { create(:namespace) }
-
- subject { namespace.use_traversal_ids_for_ancestors_upto? }
-
- context 'when use_traversal_ids_for_ancestors_upto feature flag is true' do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestors_upto: true)
- end
-
- it { is_expected.to eq true }
-
- it_behaves_like 'disabled feature flag when traversal_ids is blank'
- end
-
- context 'when use_traversal_ids_for_ancestors_upto feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestors_upto: false)
- end
-
- it { is_expected.to eq false }
- end
-
- context 'when use_traversal_ids? feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids: false)
- end
-
- it { is_expected.to eq false }
- end
- end
-
- describe '#use_traversal_ids_for_self_and_hierarchy?' do
- let_it_be(:namespace, reload: true) { create(:namespace) }
-
- subject { namespace.use_traversal_ids_for_self_and_hierarchy? }
-
- it { is_expected.to eq true }
-
- it_behaves_like 'disabled feature flag when traversal_ids is blank'
-
- context 'when use_traversal_ids_for_self_and_hierarchy feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids_for_self_and_hierarchy: false)
- end
-
- it { is_expected.to eq false }
- end
-
- context 'when use_traversal_ids? feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids: false)
- end
-
- it { is_expected.to eq false }
- end
- end
-
describe '#users_with_descendants' do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
@@ -2747,4 +2644,11 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
end
+
+ context 'with loose foreign key on organization_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:organization) }
+ let!(:model) { create(:namespace, organization: parent) }
+ end
+ end
end
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
index 5fa590eab58..b21a2bf2079 100644
--- a/spec/models/oauth_access_token_spec.rb
+++ b/spec/models/oauth_access_token_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe OauthAccessToken do
end
it 'finds a token by plaintext token' do
- expect(described_class.by_token(token.plaintext_token)).to be_a(OauthAccessToken)
+ expect(described_class.by_token(token.plaintext_token)).to be_a(described_class)
end
context 'when the token is stored in plaintext' do
@@ -43,7 +43,7 @@ RSpec.describe OauthAccessToken do
end
it 'falls back to plaintext token comparison' do
- expect(described_class.by_token(plaintext_token)).to be_a(OauthAccessToken)
+ expect(described_class.by_token(plaintext_token)).to be_a(described_class)
end
end
end
@@ -57,7 +57,7 @@ RSpec.describe OauthAccessToken do
describe '#expires_in' do
context 'when token has expires_in value set' do
it 'uses the expires_in value' do
- token = OauthAccessToken.new(expires_in: 1.minute)
+ token = described_class.new(expires_in: 1.minute)
expect(token).to be_valid
end
@@ -65,7 +65,7 @@ RSpec.describe OauthAccessToken do
context 'when token has nil expires_in' do
it 'uses default value' do
- token = OauthAccessToken.new(expires_in: nil)
+ token = described_class.new(expires_in: nil)
expect(token).to be_invalid
end
diff --git a/spec/models/organizations/organization_setting_spec.rb b/spec/models/organizations/organization_setting_spec.rb
new file mode 100644
index 00000000000..376d0b7fe77
--- /dev/null
+++ b/spec/models/organizations/organization_setting_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::OrganizationSetting, type: :model, feature_category: :cell do
+ let_it_be(:organization) { create(:organization) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to :organization }
+ end
+
+ describe 'validations' do
+ context 'for json schema' do
+ let(:restricted_visibility_levels) { [] }
+ let(:settings) do
+ {
+ restricted_visibility_levels: restricted_visibility_levels
+ }
+ end
+
+ it { is_expected.to allow_value(settings).for(:settings) }
+
+ context 'when trying to store an unsupported key' do
+ let(:settings) do
+ {
+ unsupported_key: 'some_value'
+ }
+ end
+
+ it { is_expected.not_to allow_value(settings).for(:settings) }
+ end
+
+ context "when key 'restricted_visibility_levels' is invalid" do
+ let(:restricted_visibility_levels) { ['some_string'] }
+
+ it { is_expected.not_to allow_value(settings).for(:settings) }
+ end
+ end
+
+ context 'when setting restricted_visibility_levels' do
+ it 'is one or more of Gitlab::VisibilityLevel constants' do
+ setting = build(:organization_setting)
+
+ setting.restricted_visibility_levels = [123]
+
+ expect(setting.valid?).to be false
+ expect(setting.errors.full_messages).to include(
+ "Restricted visibility levels '123' is not a valid visibility level"
+ )
+
+ setting.restricted_visibility_levels = [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL]
+ expect(setting.valid?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb
index 4a75f352b6f..a9cac30e9a1 100644
--- a/spec/models/organizations/organization_spec.rb
+++ b/spec/models/organizations/organization_spec.rb
@@ -6,6 +6,13 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
let_it_be(:organization) { create(:organization) }
let_it_be(:default_organization) { create(:organization, :default) }
+ describe 'associations' do
+ it { is_expected.to have_many :namespaces }
+ it { is_expected.to have_many :groups }
+ it { is_expected.to have_many(:users).through(:organization_users).inverse_of(:organizations) }
+ it { is_expected.to have_many(:organization_users).inverse_of(:organization) }
+ end
+
describe 'validations' do
subject { create(:organization) }
@@ -141,4 +148,18 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
expect(organization.to_param).to eq('org_path')
end
end
+
+ context 'on deleting organizations via SQL' do
+ it 'does not allow to delete default organization' do
+ expect { default_organization.delete }.to raise_error(
+ ActiveRecord::StatementInvalid, /Deletion of the default Organization is not allowed/
+ )
+ end
+
+ it 'allows to delete any other organization' do
+ organization.delete
+
+ expect(described_class.where(id: organization)).not_to exist
+ end
+ end
end
diff --git a/spec/models/organizations/organization_user_spec.rb b/spec/models/organizations/organization_user_spec.rb
new file mode 100644
index 00000000000..392ffa1b5be
--- /dev/null
+++ b/spec/models/organizations/organization_user_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::OrganizationUser, type: :model, feature_category: :cell do
+ describe 'associations' do
+ it { is_expected.to belong_to(:organization).inverse_of(:organization_users).required }
+ it { is_expected.to belong_to(:user).inverse_of(:organization_users).required }
+ end
+end
diff --git a/spec/models/packages/dependency_spec.rb b/spec/models/packages/dependency_spec.rb
index 80ec7f77fda..9918a2bdb14 100644
--- a/spec/models/packages/dependency_spec.rb
+++ b/spec/models/packages/dependency_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Packages::Dependency, type: :model, feature_category: :package_re
let(:chunk_size) { 50 }
let(:rows_limit) { 50 }
- subject { Packages::Dependency.ids_for_package_names_and_version_patterns(names_and_version_patterns, chunk_size, rows_limit) }
+ subject { described_class.ids_for_package_names_and_version_patterns(names_and_version_patterns, chunk_size, rows_limit) }
it { is_expected.to match_array(expected_ids) }
@@ -97,7 +97,7 @@ RSpec.describe Packages::Dependency, type: :model, feature_category: :package_re
let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) }
- subject { Packages::Dependency.for_package_names_and_version_patterns(names_and_version_patterns) }
+ subject { described_class.for_package_names_and_version_patterns(names_and_version_patterns) }
it { is_expected.to match_array(expected_array) }
diff --git a/spec/models/packages/maven/metadatum_spec.rb b/spec/models/packages/maven/metadatum_spec.rb
index 0000543cb18..ef55bcdcd20 100644
--- a/spec/models/packages/maven/metadatum_spec.rb
+++ b/spec/models/packages/maven/metadatum_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Packages::Maven::Metadatum, type: :model do
describe '.for_package_ids' do
let_it_be(:metadata) { create_list(:maven_metadatum, 3, package: package) }
- subject { Packages::Maven::Metadatum.for_package_ids(package.id) }
+ subject { described_class.for_package_ids(package.id) }
it { is_expected.to match_array(metadata) }
end
diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb
index 418194bffdd..e5586dca15c 100644
--- a/spec/models/packages/npm/metadatum_spec.rb
+++ b/spec/models/packages/npm/metadatum_spec.rb
@@ -38,7 +38,9 @@ RSpec.describe Packages::Npm::Metadatum, type: :model, feature_category: :packag
it { is_expected.not_to allow_value({}).for(:package_json) }
- it { is_expected.not_to allow_value(test: 'test' * 10000).for(:package_json) }
+ it {
+ is_expected.not_to allow_value(test: 'test' * 10000).for(:package_json).with_message(/structure is too large/)
+ }
def with_dist
valid_json.tap do |h|
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 90a5d815427..120b7d72cd9 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -804,15 +804,6 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
let!(:package2) { create(:npm_package, version: '1.0.1') }
let!(:package3) { create(:npm_package, version: '1.0.1') }
- describe '.last_of_each_version' do
- subject { described_class.last_of_each_version }
-
- it 'includes only latest package per version' do
- is_expected.to include(package1, package3)
- is_expected.not_to include(package2)
- end
- end
-
describe '.has_version' do
subject { described_class.has_version }
@@ -1023,6 +1014,32 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
end
end
+ describe '.select_only_first_by_name' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package1) { create(:package, name: 'p1', created_at: 1000, project: project) }
+ let_it_be(:package2) { create(:package, name: 'p1', created_at: 1001, project: project) }
+ let_it_be(:package3) { create(:package, name: 'p2', project: project) }
+
+ subject { described_class.order_name_desc_version_desc.select_only_first_by_name }
+
+ it 'returns only the most recent package by name' do
+ is_expected.to eq([package3, package2])
+ end
+ end
+
+ describe '.order_name_desc_version_desc' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package1) { create(:package, name: 'p1', created_at: 1000, project: project) }
+ let_it_be(:package2) { create(:package, name: 'p1', created_at: 1001, project: project) }
+ let_it_be(:package3) { create(:package, name: 'p2', project: project) }
+
+ subject { described_class.order_name_desc_version_desc }
+
+ it 'sorts packages by name desc and created desc' do
+ is_expected.to eq([package3, package2, package1])
+ end
+ end
+
context 'sorting' do
let_it_be(:project) { create(:project, name: 'aaa') }
let_it_be(:project2) { create(:project, name: 'bbb') }
@@ -1032,22 +1049,22 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
let_it_be(:package4) { create(:package, project: project) }
it 'orders packages by their projects name ascending' do
- expect(Packages::Package.order_project_name).to eq([package1, package4, package2, package3])
+ expect(described_class.order_project_name).to eq([package1, package4, package2, package3])
end
it 'orders packages by their projects name descending' do
- expect(Packages::Package.order_project_name_desc).to eq([package2, package3, package1, package4])
+ expect(described_class.order_project_name_desc).to eq([package2, package3, package1, package4])
end
shared_examples 'order_project_path scope' do
it 'orders packages by their projects path asc, then package id asc' do
- expect(Packages::Package.order_project_path).to eq([package1, package4, package2, package3])
+ expect(described_class.order_project_path).to eq([package1, package4, package2, package3])
end
end
shared_examples 'order_project_path_desc scope' do
it 'orders packages by their projects path desc, then package id desc' do
- expect(Packages::Package.order_project_path_desc).to eq([package3, package2, package4, package1])
+ expect(described_class.order_project_path_desc).to eq([package3, package2, package4, package1])
end
end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 88fd1bd9e56..62152f9d3a4 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -8,7 +8,12 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
subject(:lookup_path) { described_class.new(project) }
before do
- stub_pages_setting(access_control: true, external_https: ["1.1.1.1:443"])
+ stub_pages_setting(
+ access_control: true,
+ external_https: ["1.1.1.1:443"],
+ url: 'http://example.com',
+ protocol: 'http'
+ )
stub_pages_object_storage(::Pages::DeploymentUploader)
end
@@ -120,18 +125,14 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
describe '#prefix' do
it 'returns "/" for pages group root projects' do
- project = instance_double(Project, pages_namespace_url: "namespace.test", pages_url: "namespace.test")
+ project = instance_double(Project, full_path: "namespace/namespace.example.com")
lookup_path = described_class.new(project, trim_prefix: 'mygroup')
expect(lookup_path.prefix).to eq('/')
end
it 'returns the project full path with the provided prefix removed' do
- project = instance_double(
- Project,
- pages_namespace_url: "namespace.test",
- pages_url: "namespace.other",
- full_path: 'mygroup/myproject')
+ project = instance_double(Project, full_path: 'mygroup/myproject')
lookup_path = described_class.new(project, trim_prefix: 'mygroup')
expect(lookup_path.prefix).to eq('/myproject/')
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index 767db511d85..553491f6eff 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -184,7 +184,7 @@ RSpec.describe PagesDeployment, feature_category: :pages do
# new deployment
create(:pages_deployment)
- expect(PagesDeployment.older_than(deployment.id)).to eq(old_deployments)
+ expect(described_class.older_than(deployment.id)).to eq(old_deployments)
end
end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 2c63306bd0a..3030756a413 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -579,7 +579,7 @@ RSpec.describe PagesDomain do
it 'lookup is case-insensitive' do
pages_domain = create(:pages_domain, domain: "Pages.IO")
- expect(PagesDomain.find_by_domain_case_insensitive('pages.io')).to eq(pages_domain)
+ expect(described_class.find_by_domain_case_insensitive('pages.io')).to eq(pages_domain)
end
end
end
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
index 21b16bdeb17..f338e5439ad 100644
--- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
subject { described_class.from_json(json_content) }
it 'creates a PrometheusDashboard object' do
- expect(subject).to be_a PerformanceMonitoring::PrometheusDashboard
+ expect(subject).to be_a described_class
expect(subject.dashboard).to eq(json_content['dashboard'])
expect(subject.panel_groups).to all(be_a PerformanceMonitoring::PrometheusPanelGroup)
end
diff --git a/spec/models/performance_monitoring/prometheus_metric_spec.rb b/spec/models/performance_monitoring/prometheus_metric_spec.rb
index b5b9cd58aa8..58bb59793cf 100644
--- a/spec/models/performance_monitoring/prometheus_metric_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_metric_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe PerformanceMonitoring::PrometheusMetric do
subject { described_class.from_json(json_content) }
it 'creates a PrometheusMetric object' do
- expect(subject).to be_a PerformanceMonitoring::PrometheusMetric
+ expect(subject).to be_a described_class
expect(subject.id).to eq(json_content['id'])
expect(subject.unit).to eq(json_content['unit'])
expect(subject.label).to eq(json_content['label'])
diff --git a/spec/models/performance_monitoring/prometheus_panel_group_spec.rb b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
index 9e92cb27954..497f80483eb 100644
--- a/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe PerformanceMonitoring::PrometheusPanelGroup do
subject { described_class.from_json(json_content) }
it 'creates a PrometheusPanelGroup object' do
- expect(subject).to be_a PerformanceMonitoring::PrometheusPanelGroup
+ expect(subject).to be_a described_class
expect(subject.group).to eq(json_content['group'])
expect(subject.panels).to all(be_a PerformanceMonitoring::PrometheusPanel)
end
diff --git a/spec/models/performance_monitoring/prometheus_panel_spec.rb b/spec/models/performance_monitoring/prometheus_panel_spec.rb
index c5c6b1fdafd..42dcbbdb8e0 100644
--- a/spec/models/performance_monitoring/prometheus_panel_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_panel_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe PerformanceMonitoring::PrometheusPanel do
subject { described_class.from_json(json_content) }
it 'creates a PrometheusPanelGroup object' do
- expect(subject).to be_a PerformanceMonitoring::PrometheusPanel
+ expect(subject).to be_a described_class
expect(subject.type).to eq(json_content['type'])
expect(subject.title).to eq(json_content['title'])
expect(subject.y_label).to eq(json_content['y_label'])
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 8e86518912c..7437e9b463e 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -21,6 +21,12 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
end
end
+ describe 'associations' do
+ subject(:project_access_token) { create(:personal_access_token) }
+
+ it { is_expected.to belong_to(:previous_personal_access_token).class_name('PersonalAccessToken') }
+ end
+
describe 'scopes' do
describe '.project_access_tokens' do
let_it_be(:user) { create(:user, :project_bot) }
@@ -257,7 +263,7 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
end
context 'validates expires_at' do
- let(:max_expiration_date) { described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now }
+ let(:max_expiration_date) { Date.current + described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS }
it "can't be blank" do
personal_access_token.expires_at = nil
@@ -274,12 +280,14 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
end
end
- context 'when expires_in is more than MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS days' do
+ context 'when expires_in is more than MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS days', :freeze_time do
it 'is invalid' do
personal_access_token.expires_at = max_expiration_date + 1.day
expect(personal_access_token).not_to be_valid
- expect(personal_access_token.errors[:expires_at].first).to eq('must expire in 365 days')
+ expect(personal_access_token.errors.full_messages.to_sentence).to eq(
+ "Expiration date must be before #{max_expiration_date}"
+ )
end
end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index d211499e9e9..bee1c4f47b0 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -302,94 +302,136 @@ RSpec.describe PlanLimits do
end
end
- describe '#log_limits_changes', :freeze_time do
+ describe '#format_limits_history', :freeze_time do
let(:user) { create(:user) }
let(:plan_limits) { create(:plan_limits) }
let(:current_timestamp) { Time.current.utc.to_i }
- let(:history) { plan_limits.limits_history }
- it 'logs a single attribute change' do
- plan_limits.log_limits_changes(user, enforcement_limit: 5_000)
-
- expect(history).to eq(
- { 'enforcement_limit' => [{ 'user_id' => user.id, 'username' => user.username,
- 'timestamp' => current_timestamp, 'value' => 5_000 }] }
+ it 'formats a single attribute change' do
+ formatted_limits_history = plan_limits.format_limits_history(user, enforcement_limit: 5_000)
+
+ expect(formatted_limits_history).to eq(
+ {
+ "enforcement_limit" => [
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 5000
+ }
+ ]
+ }
)
end
- it 'logs multiple attribute changes' do
- plan_limits.log_limits_changes(user, enforcement_limit: 10_000, notification_limit: 20_000)
+ it 'does not format limits_history for non-allowed attributes' do
+ formatted_limits_history = plan_limits.format_limits_history(user,
+ { enforcement_limit: 20_000, pipeline_hierarchy_size: 10_000 })
- expect(history).to eq(
- { 'enforcement_limit' => [{ 'user_id' => user.id, 'username' => user.username,
- 'timestamp' => current_timestamp, 'value' => 10_000 }],
- 'notification_limit' => [{ 'user_id' => user.id, 'username' => user.username,
- 'timestamp' => current_timestamp,
- 'value' => 20_000 }] }
- )
+ expect(formatted_limits_history).to eq({
+ "enforcement_limit" => [
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 20_000
+ }
+ ]
+ })
end
- it 'allows logging dashboard_limit_enabled_at from console (without user)' do
- plan_limits.log_limits_changes(nil, dashboard_limit_enabled_at: current_timestamp)
+ it 'does not format attributes for values that do not change' do
+ plan_limits.update!(enforcement_limit: 20_000)
+ formatted_limits_history = plan_limits.format_limits_history(user, enforcement_limit: 20_000)
- expect(history).to eq(
- { 'dashboard_limit_enabled_at' => [{ 'user_id' => nil, 'username' => nil, 'timestamp' => current_timestamp,
- 'value' => current_timestamp }] }
+ expect(formatted_limits_history).to eq({})
+ end
+
+ it 'formats multiple attribute changes' do
+ formatted_limits_history = plan_limits.format_limits_history(user, enforcement_limit: 10_000,
+ notification_limit: 20_000, dashboard_limit_enabled_at: current_timestamp)
+
+ expect(formatted_limits_history).to eq(
+ {
+ "notification_limit" => [
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 20000
+ }
+ ],
+ "enforcement_limit" => [
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 10000
+ }
+ ],
+ "dashboard_limit_enabled_at" => [
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => current_timestamp
+ }
+ ]
+ }
)
end
- context 'with previous history avilable' do
+ context 'with previous history available' do
let(:plan_limits) do
- create(:plan_limits,
- limits_history: { 'enforcement_limit' => [{ user_id: user.id, username: user.username,
- timestamp: current_timestamp,
- value: 20_000 },
- { user_id: user.id, username: user.username, timestamp: current_timestamp,
- value: 50_000 }] })
+ create(
+ :plan_limits,
+ limits_history: {
+ 'enforcement_limit' => [
+ {
+ user_id: user.id,
+ username: user.username,
+ timestamp: current_timestamp,
+ value: 20_000
+ },
+ {
+ user_id: user.id,
+ username: user.username,
+ timestamp: current_timestamp,
+ value: 50_000
+ }
+ ]
+ }
+ )
end
it 'appends to it' do
- plan_limits.log_limits_changes(user, enforcement_limit: 60_000)
- expect(history).to eq(
+ formatted_limits_history = plan_limits.format_limits_history(user, enforcement_limit: 60_000)
+
+ expect(formatted_limits_history).to eq(
{
- 'enforcement_limit' => [
- { 'user_id' => user.id, 'username' => user.username, 'timestamp' => current_timestamp,
- 'value' => 20_000 },
- { 'user_id' => user.id, 'username' => user.username, 'timestamp' => current_timestamp,
- 'value' => 50_000 },
- { 'user_id' => user.id, 'username' => user.username, 'timestamp' => current_timestamp, 'value' => 60_000 }
+ "enforcement_limit" => [
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 20000
+ },
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 50000
+ },
+ {
+ "user_id" => user.id,
+ "username" => user.username,
+ "timestamp" => current_timestamp,
+ "value" => 60000
+ }
]
}
)
end
end
end
-
- describe '#limit_attribute_changes', :freeze_time do
- let(:user) { create(:user) }
- let(:current_timestamp) { Time.current.utc.to_i }
- let(:plan_limits) do
- create(:plan_limits,
- limits_history: { 'enforcement_limit' => [
- { user_id: user.id, username: user.username, timestamp: current_timestamp,
- value: 20_000 }, { user_id: user.id, username: user.username, timestamp: current_timestamp,
- value: 50_000 }
- ] })
- end
-
- it 'returns an empty array for attribute with no changes' do
- changes = plan_limits.limit_attribute_changes(:notification_limit)
-
- expect(changes).to eq([])
- end
-
- it 'returns the changes for a specific attribute' do
- changes = plan_limits.limit_attribute_changes(:enforcement_limit)
-
- expect(changes).to eq(
- [{ timestamp: current_timestamp, value: 20_000, username: user.username, user_id: user.id },
- { timestamp: current_timestamp, value: 50_000, username: user.username, user_id: user.id }]
- )
- end
- end
end
diff --git a/spec/models/postgresql/detached_partition_spec.rb b/spec/models/postgresql/detached_partition_spec.rb
index aaa99e842b4..d9e77b70368 100644
--- a/spec/models/postgresql/detached_partition_spec.rb
+++ b/spec/models/postgresql/detached_partition_spec.rb
@@ -4,15 +4,15 @@ require 'spec_helper'
RSpec.describe Postgresql::DetachedPartition do
describe '#ready_to_drop' do
- let_it_be(:drop_before) { Postgresql::DetachedPartition.create!(drop_after: 1.day.ago, table_name: 'old_table') }
- let_it_be(:drop_after) { Postgresql::DetachedPartition.create!(drop_after: 1.day.from_now, table_name: 'new_table') }
+ let_it_be(:drop_before) { described_class.create!(drop_after: 1.day.ago, table_name: 'old_table') }
+ let_it_be(:drop_after) { described_class.create!(drop_after: 1.day.from_now, table_name: 'new_table') }
it 'includes partitions that should be dropped before now' do
- expect(Postgresql::DetachedPartition.ready_to_drop.to_a).to include(drop_before)
+ expect(described_class.ready_to_drop.to_a).to include(drop_before)
end
it 'does not include partitions that should be dropped after now' do
- expect(Postgresql::DetachedPartition.ready_to_drop.to_a).not_to include(drop_after)
+ expect(described_class.ready_to_drop.to_a).not_to include(drop_after)
end
end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
index a2ab59f56ab..17db284c61e 100644
--- a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
context 'when user is present' do
before do
- Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_arg, user).execute
+ described_class.new(projects_arg, user).execute
end
it 'avoids N+1 queries' do
@@ -61,7 +61,7 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
context 'when user is not present' do
before do
- Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_arg, nil).execute
+ described_class.new(projects_arg, nil).execute
end
it 'does not avoid N+1 queries' do
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index ba9ea759c6a..62839f5fb4f 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -107,14 +107,14 @@ RSpec.describe ProjectLabel do
context 'using name' do
it 'returns cross reference with label name' do
expect(label.to_reference(project, format: :name))
- .to eq %Q(#{label.project.full_path}~"#{label.name}")
+ .to eq %(#{label.project.full_path}~"#{label.name}")
end
end
context 'using id' do
it 'returns cross reference with label id' do
expect(label.to_reference(project, format: :id))
- .to eq %Q(#{label.project.full_path}~#{label.id})
+ .to eq %(#{label.project.full_path}~#{label.id})
end
end
end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 4b2760d7699..6928cc8ba08 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -27,8 +27,7 @@ RSpec.describe ProjectSetting, type: :model, feature_category: :groups_and_proje
it { is_expected.to validate_length_of(:issue_branch_template).is_at_most(255) }
it { is_expected.not_to allow_value(nil).for(:suggested_reviewers_enabled) }
- it { is_expected.to allow_value(true).for(:suggested_reviewers_enabled) }
- it { is_expected.to allow_value(false).for(:suggested_reviewers_enabled) }
+ it { is_expected.to allow_value(true, false).for(:suggested_reviewers_enabled) }
it 'allows any combination of the allowed target platforms' do
valid_target_platform_combinations.each do |target_platforms|
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f44331521e9..044408e86e9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -342,8 +342,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
context 'when same project is being updated in 2 instances' do
it 'syncs only changed attributes' do
- project1 = Project.last
- project2 = Project.last
+ project1 = described_class.last
+ project2 = described_class.last
project_name = project1.name
project_path = project1.path
@@ -2170,6 +2170,32 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ describe '.with_slack_integration' do
+ it 'returns projects with both active and inactive slack integrations' do
+ create(:project)
+ with_active_slack = create(:integrations_slack).project
+ with_disabled_slack = create(:integrations_slack, active: false).project
+
+ expect(described_class.with_slack_integration).to contain_exactly(
+ with_active_slack,
+ with_disabled_slack
+ )
+ end
+ end
+
+ describe '.with_slack_slash_commands_integration' do
+ it 'returns projects with both active and inactive slack slash commands integrations' do
+ create(:project)
+ with_active_slash_commands = create(:slack_slash_commands_integration).project
+ with_disabled_slash_commands = create(:slack_slash_commands_integration, active: false).project
+
+ expect(described_class.with_slack_slash_commands_integration).to contain_exactly(
+ with_active_slash_commands,
+ with_disabled_slash_commands
+ )
+ end
+ end
+
describe '.cached_count', :use_clean_rails_memory_store_caching do
let(:group) { create(:group, :public) }
let!(:project1) { create(:project, :public, group: group) }
@@ -2382,11 +2408,11 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
create(:service_desk_setting, project_key: 'key2')
create(:service_desk_setting)
- expect(Project.with_service_desk_key('key1')).to contain_exactly(project1, project2)
+ expect(described_class.with_service_desk_key('key1')).to contain_exactly(project1, project2)
end
it 'returns empty if there is no project with the key' do
- expect(Project.with_service_desk_key('key1')).to be_empty
+ expect(described_class.with_service_desk_key('key1')).to be_empty
end
end
@@ -2439,7 +2465,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
create(:jira_integration, project: project_3, inherit_from_id: nil)
create(:integrations_slack, project: project_4, inherit_from_id: nil)
- expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
+ expect(described_class.without_integration(instance_integration)).to contain_exactly(project_4)
end
end
@@ -2780,224 +2806,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
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
- create(
- :project,
- namespace: group,
- name: project_name,
- path: project_path)
- end
-
- let(:domain) { 'Example.com' }
- let(:port) { nil }
-
- 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', port].compact.join(':'))
- end
-
- context 'when not using pages_unique_domain' do
- subject { project.pages_url(with_unique_domain: false) }
-
- 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/project') }
- end
-
- context 'when pages_unique_domain feature flag is enabled' do
- before do
- stub_feature_flags(pages_unique_domain: true)
-
- project.project_setting.update!(
- pages_unique_domain_enabled: pages_unique_domain_enabled,
- pages_unique_domain: 'unique-domain'
- )
- end
-
- 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') }
- end
-
- context 'when pages_unique_domain_enabled is true' do
- let(:pages_unique_domain_enabled) { true }
-
- it { is_expected.to eq('http://group.example.com/project') }
- end
- end
- end
-
- context 'when using pages_unique_domain' do
- subject { project.pages_url(with_unique_domain: true) }
-
- 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/project') }
- end
-
- context 'when pages_unique_domain feature flag is enabled' do
- before do
- stub_feature_flags(pages_unique_domain: true)
-
- project.project_setting.update!(
- pages_unique_domain_enabled: pages_unique_domain_enabled,
- pages_unique_domain: 'unique-domain'
- )
- end
-
- 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') }
- end
-
- context 'when pages_unique_domain_enabled is true' do
- let(:pages_unique_domain_enabled) { true }
-
- it { is_expected.to eq('http://unique-domain.example.com') }
- end
- end
- 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(: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
-
- context 'when the project matches its namespace url' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq('http://group.example.com') }
-
- context 'with different group name capitalization' do
- let(:group_name) { 'Group' }
-
- it { is_expected.to eq("http://group.example.com") }
- end
-
- context 'with different project path capitalization' do
- let(:project_path) { 'Group.example.com' }
-
- it { is_expected.to eq("http://group.example.com") }
- end
-
- context 'with different project name capitalization' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq("http://group.example.com/project") }
- 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
-
- it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
- end
-
- 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_unique_host', 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_host).to eq('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) }
- let(:domain) { 'Example.com' }
- let(:port) { 1234 }
-
- subject { project.pages_namespace_url }
-
- before do
- allow(Settings.pages).to receive(:host).and_return(domain)
- allow(Gitlab.config.pages).to receive(:url).and_return("http://example.com:#{port}")
- end
-
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq("http://group.example.com:#{port}") }
- end
-
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq("http://group.example.com:#{port}") }
- end
- end
-
describe '.search' do
let_it_be(:project) { create(:project, description: 'kitten mittens') }
@@ -6311,6 +6119,36 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
expect(recorder.count).to be_zero
end
+
+ context 'with a CI integration' do
+ let!(:ci_integration) do
+ create(:jenkins_integration, push_events: true, active: true, project: integration.project)
+ end
+
+ it 'executes the integrations' do
+ [Integrations::Jenkins, Integrations::Slack].each do |integration_type|
+ expect_next_found_instance_of(integration_type) do |instance|
+ expect(instance).to receive(:async_execute).with('data').once
+ end
+ end
+
+ integration.project.execute_integrations('data', :push_hooks)
+ end
+
+ context 'and skipping ci' do
+ it 'does not execute ci integrations' do
+ expect_next_found_instance_of(Integrations::Jenkins) do |instance|
+ expect(instance).not_to receive(:async_execute)
+ end
+
+ expect_next_found_instance_of(Integrations::Slack) do |instance|
+ expect(instance).to receive(:async_execute).with('data').once
+ end
+
+ integration.project.execute_integrations('data', :push_hooks, skip_ci: true)
+ end
+ end
+ end
end
describe '#has_active_hooks?' do
@@ -6338,6 +6176,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
expect(project.has_active_hooks?(:merge_request_hooks)).to eq(true)
expect(project.has_active_hooks?).to eq(true)
end
+
+ context 'with :emoji_hooks scope' do
+ it 'returns true when a matching emoji hook exists' do
+ create(:project_hook, emoji_events: true, project: project)
+
+ expect(project.has_active_hooks?(:emoji_hooks)).to eq(true)
+ end
+ end
end
describe '#has_active_integrations?' do
@@ -6576,7 +6422,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it 'does not allow access to branches for which the merge request was closed' do
create(
- :merge_request, :closed,
+ :merge_request,
+ :closed,
target_project: target_project,
target_branch: 'target-branch',
source_project: project,
@@ -7542,7 +7389,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
describe 'with_issues_or_mrs_available_for_user' do
before do
- Project.delete_all
+ described_class.delete_all
end
it 'returns correct projects' do
@@ -9028,6 +8875,67 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ describe '.without_created_and_owned_by_banned_user' do
+ let_it_be(:other_project) { create(:project) }
+
+ subject(:results) { described_class.without_created_and_owned_by_banned_user }
+
+ context 'when project creator is not banned' do
+ let_it_be(:project_of_active_user) { create(:project, creator: create(:user)) }
+
+ it 'includes the project' do
+ expect(results).to match_array([other_project, project_of_active_user])
+ end
+ end
+
+ context 'when project creator is banned' do
+ let_it_be(:banned_user) { create(:user, :banned) }
+ let_it_be(:project_of_banned_user) { create(:project, creator: banned_user) }
+
+ context 'when project creator is also an owner' do
+ let_it_be(:project_auth) do
+ project = project_of_banned_user
+ create(:project_authorization, :owner, user: project.creator, project: project)
+ end
+
+ it 'excludes the project' do
+ expect(results).to match_array([other_project])
+ end
+ end
+
+ context 'when project creator is not an owner' do
+ it 'includes the project' do
+ expect(results).to match_array([other_project, project_of_banned_user])
+ end
+ end
+ end
+ end
+
+ describe '#created_and_owned_by_banned_user?' do
+ subject { project.created_and_owned_by_banned_user? }
+
+ context 'when creator is banned' do
+ let_it_be(:creator) { create(:user, :banned) }
+ let_it_be(:project) { create(:project, creator: creator) }
+
+ it { is_expected.to eq false }
+
+ context 'when creator is an owner' do
+ let_it_be(:project_auth) do
+ create(:project_authorization, :owner, user: project.creator, project: project)
+ end
+
+ it { is_expected.to eq true }
+ end
+ end
+
+ context 'when creator is not banned' do
+ let_it_be(:project) { create(:project) }
+
+ it { is_expected.to eq false }
+ end
+ end
+
it_behaves_like 'something that has web-hooks' do
let_it_be_with_reload(:object) { create(:project) }
@@ -9081,7 +8989,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
def create_build(new_pipeline = pipeline, name = 'test')
create(
- :ci_build, :success, :artifacts,
+ :ci_build,
+ :success,
+ :artifacts,
pipeline: new_pipeline,
status: new_pipeline.status,
name: name
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index a24903f8b4e..71c205fca7c 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -360,7 +360,6 @@ RSpec.describe ProjectStatistics do
wiki_size: 4,
lfs_objects_size: 3,
snippets_size: 2,
- pipeline_artifacts_size: 3,
build_artifacts_size: 3,
packages_size: 6,
uploads_size: 5
@@ -368,7 +367,7 @@ RSpec.describe ProjectStatistics do
statistics.reload
- expect(statistics.storage_size).to eq 28
+ expect(statistics.storage_size).to eq 25
end
it 'excludes the container_registry_size' do
@@ -383,6 +382,18 @@ RSpec.describe ProjectStatistics do
expect(statistics.storage_size).to eq 7
end
+ it 'excludes the pipeline_artifacts_size' do
+ statistics.update!(
+ repository_size: 2,
+ uploads_size: 5,
+ pipeline_artifacts_size: 10
+ )
+
+ statistics.reload
+
+ expect(statistics.storage_size).to eq 7
+ end
+
it 'works during wiki_size backfill' do
statistics.update!(
repository_size: 2,
@@ -428,7 +439,7 @@ RSpec.describe ProjectStatistics do
storage_size: 0
)
- expect { refresh_storage_size }.to change { statistics.reload.storage_size }.from(0).to(28)
+ expect { refresh_storage_size }.to change { statistics.reload.storage_size }.from(0).to(25)
end
context 'when nullable columns are nil' do
@@ -464,10 +475,9 @@ RSpec.describe ProjectStatistics do
.by(increment.amount)
end
- it 'increases also storage size by that amount' do
+ it 'does not increase the storage size by that amount' do
expect { described_class.increment_statistic(project, stat, increment) }
- .to change { statistics.reload.storage_size }
- .by(increment.amount)
+ .not_to change { statistics.reload.storage_size }
end
it 'schedules a namespace aggregation worker' do
@@ -572,10 +582,9 @@ RSpec.describe ProjectStatistics do
.by(total_amount)
end
- it 'increases also storage size by that amount' do
+ it 'does not increase the storage size by that amount' do
expect { described_class.bulk_increment_statistic(project, stat, increments) }
- .to change { statistics.reload.storage_size }
- .by(total_amount)
+ .not_to change { statistics.reload.storage_size }
end
it 'schedules a namespace aggregation worker' do
diff --git a/spec/models/projects/topic_spec.rb b/spec/models/projects/topic_spec.rb
index d0bda6f51a1..568a4166de7 100644
--- a/spec/models/projects/topic_spec.rb
+++ b/spec/models/projects/topic_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe Projects::Topic do
it { is_expected.to validate_uniqueness_of(:name).case_insensitive }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
- it { expect(Projects::Topic.new).to validate_presence_of(:title) }
- it { expect(Projects::Topic.new).to validate_length_of(:title).is_at_most(255) }
+ it { expect(described_class.new).to validate_presence_of(:title) }
+ it { expect(described_class.new).to validate_length_of(:title).is_at_most(255) }
it { is_expected.not_to allow_value("new\nline").for(:name).with_message(name_format_message) }
it { is_expected.not_to allow_value("new\rline").for(:name).with_message(name_format_message) }
it { is_expected.not_to allow_value("new\vline").for(:name).with_message(name_format_message) }
diff --git a/spec/models/projects/triggered_hooks_spec.rb b/spec/models/projects/triggered_hooks_spec.rb
index 3c885bdac8e..581ccb500e2 100644
--- a/spec/models/projects/triggered_hooks_spec.rb
+++ b/spec/models/projects/triggered_hooks_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::TriggeredHooks do
+RSpec.describe Projects::TriggeredHooks, feature_category: :webhooks do
let_it_be(:project) { create(:project) }
let_it_be(:universal_push_hook) { create(:project_hook, project: project, push_events: true) }
@@ -10,6 +10,7 @@ RSpec.describe Projects::TriggeredHooks do
let_it_be(:issues_hook) { create(:project_hook, project: project, issues_events: true, push_events: false) }
let(:wh_service) { instance_double(::WebHookService, async_execute: true) }
+ let(:data) { { some: 'data', as: 'json' } }
def run_hooks(scope, data)
hooks = described_class.new(scope, data)
@@ -18,8 +19,6 @@ RSpec.describe Projects::TriggeredHooks do
end
it 'executes hooks by scope' do
- data = { some: 'data', as: 'json' }
-
expect_hook_execution(issues_hook, data, 'issue_hooks')
run_hooks(:issue_hooks, data)
@@ -42,6 +41,40 @@ RSpec.describe Projects::TriggeredHooks do
run_hooks(:push_hooks, data)
end
+ context 'with emoji hooks' do
+ let_it_be(:emoji_hook) { create(:project_hook, project: project, emoji_events: true) }
+
+ it 'executes hook' do
+ expect_hook_execution(emoji_hook, data, 'emoji_hooks')
+
+ run_hooks(:emoji_hooks, data)
+ end
+
+ context 'when emoji_webhooks feature flag is disabled' do
+ before do
+ stub_feature_flags(emoji_webhooks: false)
+ end
+
+ it 'does not execute the hook' do
+ expect(WebHookService).not_to receive(:new)
+
+ run_hooks(:emoji_hooks, data)
+ end
+ end
+
+ context 'when emoji_webhooks feature flag is enabled for the project' do
+ before do
+ stub_feature_flags(emoji_webhooks: emoji_hook.project)
+ end
+
+ it 'executes the hook' do
+ expect_hook_execution(emoji_hook, data, 'emoji_hooks')
+
+ run_hooks(:emoji_hooks, data)
+ end
+ end
+ end
+
def expect_hook_execution(hook, data, scope)
expect(WebHookService).to receive(:new).with(hook, data, scope).and_return(wh_service)
end
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
index e56ff2241b1..05e10fd6763 100644
--- a/spec/models/protected_branch/push_access_level_spec.rb
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -4,81 +4,6 @@ require 'spec_helper'
RSpec.describe ProtectedBranch::PushAccessLevel, feature_category: :source_code_management do
include_examples 'protected branch access'
+ include_examples 'protected ref deploy_key access'
include_examples 'protected ref access allowed_access_levels'
-
- describe 'associations' do
- it { is_expected.to belong_to(:deploy_key) }
- end
-
- describe 'validations' do
- it 'is not valid when a record exists with the same access level' do
- protected_branch = create(:protected_branch)
- create(:protected_branch_push_access_level, protected_branch: protected_branch)
- level = build(:protected_branch_push_access_level, protected_branch: protected_branch)
-
- expect(level).to be_invalid
- end
-
- it 'is not valid when a record exists with the same access level' do
- protected_branch = create(:protected_branch)
- deploy_key = create(:deploy_key, projects: [protected_branch.project])
- create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key)
- level = build(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key)
-
- expect(level).to be_invalid
- end
-
- it 'checks that a deploy key is enabled for the same project as the protected branch\'s' do
- level = build(:protected_branch_push_access_level, deploy_key: create(:deploy_key))
-
- expect { level.save! }.to raise_error(ActiveRecord::RecordInvalid)
- expect(level.errors.full_messages).to contain_exactly('Deploy key is not enabled for this project')
- end
- end
-
- describe '#check_access' do
- let_it_be(:project) { create(:project) }
- let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_push, project: project) }
- let_it_be(:user) { create(:user) }
- let_it_be(:deploy_key) { create(:deploy_key, user: user) }
-
- let!(:deploy_keys_project) { create(:deploy_keys_project, project: project, deploy_key: deploy_key, can_push: can_push) }
- let(:can_push) { true }
-
- before_all do
- project.add_maintainer(user)
- end
-
- context 'when this push_access_level is tied to a deploy key' do
- let(:push_access_level) { create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) }
-
- context 'when the deploy key is among the active keys for this project' do
- specify do
- expect(push_access_level.check_access(user)).to be_truthy
- end
- end
-
- context 'when the deploy key is not among the active keys of this project' do
- let(:can_push) { false }
-
- it 'is false' do
- expect(push_access_level.check_access(user)).to be_falsey
- end
- end
- end
- end
-
- describe '#type' do
- let(:push_level_access) { build(:protected_branch_push_access_level) }
-
- it 'returns :deploy_key when a deploy key is tied to the protected branch' do
- push_level_access.deploy_key = create(:deploy_key)
-
- expect(push_level_access.type).to eq(:deploy_key)
- end
-
- it 'returns :role by default' do
- expect(push_level_access.type).to eq(:role)
- end
- end
end
diff --git a/spec/models/protected_tag/create_access_level_spec.rb b/spec/models/protected_tag/create_access_level_spec.rb
index 8eeccdc9b34..d795399060e 100644
--- a/spec/models/protected_tag/create_access_level_spec.rb
+++ b/spec/models/protected_tag/create_access_level_spec.rb
@@ -4,134 +4,6 @@ require 'spec_helper'
RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_management do
include_examples 'protected tag access'
+ include_examples 'protected ref deploy_key access'
include_examples 'protected ref access allowed_access_levels'
-
- describe 'associations' do
- it { is_expected.to belong_to(:deploy_key) }
- end
-
- describe 'validations', :aggregate_failures do
- let_it_be(:protected_tag) { create(:protected_tag) }
-
- context 'when deploy key enabled for the project' do
- let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
-
- it 'is valid' do
- level = build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
-
- expect(level).to be_valid
- end
- end
-
- context 'when a record exists with the same access level' do
- before do
- create(:protected_tag_create_access_level, protected_tag: protected_tag)
- end
-
- it 'is not valid' do
- level = build(:protected_tag_create_access_level, protected_tag: protected_tag)
-
- expect(level).to be_invalid
- expect(level.errors.full_messages).to include('Access level has already been taken')
- end
- end
-
- context 'when a deploy key already added for this access level' do
- let!(:create_access_level) do
- create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
- end
-
- let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
-
- it 'is not valid' do
- level = build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
-
- expect(level).to be_invalid
- expect(level.errors.full_messages).to contain_exactly('Deploy key has already been taken')
- end
- end
-
- context 'when deploy key is not enabled for the project' do
- let(:create_access_level) do
- build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: create(:deploy_key))
- end
-
- it 'returns an error' do
- expect(create_access_level).to be_invalid
- expect(create_access_level.errors.full_messages).to contain_exactly(
- 'Deploy key is not enabled for this project'
- )
- end
- end
- end
-
- describe '#check_access' do
- let_it_be(:project) { create(:project) }
- let_it_be(:protected_tag) { create(:protected_tag, :no_one_can_create, project: project) }
- let_it_be(:user) { create(:user) }
- let_it_be(:deploy_key) { create(:deploy_key, user: user) }
-
- let!(:deploy_keys_project) do
- create(:deploy_keys_project, project: project, deploy_key: deploy_key, can_push: can_push)
- end
-
- let(:create_access_level) { protected_tag.create_access_levels.first }
- let(:can_push) { true }
-
- before_all do
- project.add_maintainer(user)
- end
-
- it { expect(create_access_level.check_access(user)).to be_falsey }
-
- context 'when this create_access_level is tied to a deploy key' do
- let(:create_access_level) do
- create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
- end
-
- context 'when the deploy key is among the active keys for this project' do
- it { expect(create_access_level.check_access(user)).to be_truthy }
- end
-
- context 'when user is missing' do
- it { expect(create_access_level.check_access(nil)).to be_falsey }
- end
-
- context 'when deploy key does not belong to the user' do
- let(:another_user) { create(:user) }
-
- it { expect(create_access_level.check_access(another_user)).to be_falsey }
- end
-
- context 'when user cannot access the project' do
- before do
- allow(user).to receive(:can?).with(:read_project, project).and_return(false)
- end
-
- it { expect(create_access_level.check_access(user)).to be_falsey }
- end
-
- context 'when the deploy key is not among the active keys of this project' do
- let(:can_push) { false }
-
- it { expect(create_access_level.check_access(user)).to be_falsey }
- end
- end
- end
-
- describe '#type' do
- let(:create_access_level) { build(:protected_tag_create_access_level) }
-
- it 'returns :role by default' do
- expect(create_access_level.type).to eq(:role)
- end
-
- context 'when a deploy key is tied to the protected branch' do
- let(:create_access_level) { build(:protected_tag_create_access_level, deploy_key: build(:deploy_key)) }
-
- it 'returns :deploy_key' do
- expect(create_access_level.type).to eq(:deploy_key)
- end
- end
- end
end
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index 50a607040b6..6369f0e5c26 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -12,12 +12,12 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
end
after do
- ReleaseHighlight.instance_variable_set(:@file_paths, nil)
+ described_class.instance_variable_set(:@file_paths, nil)
end
describe '.paginated_query' do
context 'with page param' do
- subject { ReleaseHighlight.paginated_query(page: page) }
+ subject { described_class.paginated_query(page: page) }
context 'when there is another page of results' do
let(:page) { 3 }
@@ -49,7 +49,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
describe '.paginated' do
context 'with no page param' do
- subject { ReleaseHighlight.paginated }
+ subject { described_class.paginated }
it 'uses multiple levels of cache' do
expect(Rails.cache).to receive(:fetch).with("release_highlight:all_tiers:items:page-1:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
@@ -100,7 +100,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
end
describe '.most_recent_item_count' do
- subject { ReleaseHighlight.most_recent_item_count }
+ subject { described_class.most_recent_item_count }
it 'uses process memory cache' do
expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:all_tiers:recent_item_count:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
@@ -110,7 +110,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
context 'when recent release items exist' do
it 'returns the count from the most recent file' do
- allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
+ allow(described_class).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
expect(subject).to eq(1)
end
@@ -118,7 +118,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
context 'when recent release items do NOT exist' do
it 'returns nil' do
- allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
+ allow(described_class).to receive(:paginated).and_return(nil)
expect(subject).to be_nil
end
@@ -126,7 +126,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
end
describe '.most_recent_version_digest' do
- subject { ReleaseHighlight.most_recent_version_digest }
+ subject { described_class.most_recent_version_digest }
it 'uses process memory cache' do
expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:all_tiers:most_recent_version_digest:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
@@ -143,7 +143,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
context 'when recent release items do NOT exist' do
it 'returns nil' do
- allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
+ allow(described_class).to receive(:paginated).and_return(nil)
expect(subject).to be_nil
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index bddd0516400..446ef4180d2 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -174,7 +174,7 @@ RSpec.describe Release, feature_category: :release_orchestration do
end
describe '#assets_count' do
- subject { Release.find(release.id).assets_count }
+ subject { described_class.find(release.id).assets_count }
it 'returns the number of sources' do
is_expected.to eq(Gitlab::Workhorse::ARCHIVE_FORMATS.count)
@@ -188,7 +188,7 @@ RSpec.describe Release, feature_category: :release_orchestration do
end
it "excludes sources count when asked" do
- assets_count = Release.find(release.id).assets_count(except: [:sources])
+ assets_count = described_class.find(release.id).assets_count(except: [:sources])
expect(assets_count).to eq(1)
end
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 382718620f5..537fdbc7c8f 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -289,6 +289,14 @@ RSpec.describe RemoteMirror, :mailer do
end
end
+ context 'with silent mode enabled' do
+ it 'returns nil' do
+ allow(Gitlab::SilentMode).to receive(:enabled?).and_return(true)
+
+ expect(remote_mirror.sync).to be_nil
+ end
+ end
+
context 'with remote mirroring enabled' do
it 'defaults to disabling only protected branches' do
expect(remote_mirror.only_protected_branches?).to be_falsey
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 929eaca85f7..0bdaa4994e5 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -281,14 +281,14 @@ RSpec.describe Route do
it 'does not delete the original route' do
# before deleting the route, check its there
- expect(Route.where(path: offending_route.path).count).to eq(1)
+ expect(described_class.where(path: offending_route.path).count).to eq(1)
expect do
Group.delete(conflicting_group) # delete group with conflicting route
end.to change { described_class.count }.by(-1)
# check the conflicting route is gone
- expect(Route.where(path: offending_route.path).count).to eq(0)
+ expect(described_class.where(path: offending_route.path).count).to eq(0)
expect(route.path).to eq(offending_route.path)
expect(route.valid?).to be_truthy
end
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index b9679b82bd0..34165fc2bf3 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -3,12 +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) }
+ subject(:setting) { build(:service_desk_setting) }
describe 'validations' do
- subject(:service_desk_setting) { create(:service_desk_setting) }
-
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_length_of(:outgoing_name).is_at_most(255) }
it { is_expected.to validate_length_of(:project_key).is_at_most(255) }
@@ -18,14 +15,56 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
it { is_expected.to validate_length_of(:custom_email).is_at_most(255) }
describe '#custom_email_enabled' do
- it { expect(subject.custom_email_enabled).to be_falsey }
+ it { expect(setting.custom_email_enabled).to be_falsey }
it { expect(described_class.new(custom_email_enabled: true).custom_email_enabled).to be_truthy }
+
+ context 'when set to true' do
+ let(:expected_error_part) { 'cannot be enabled until verification process has finished.' }
+
+ before do
+ setting.custom_email = 'user@example.com'
+ setting.custom_email_enabled = true
+ end
+
+ it 'is not valid' do
+ is_expected.not_to be_valid
+ expect(setting.errors[:custom_email_enabled].join).to include(expected_error_part)
+ end
+
+ context 'when custom email records exist' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:credential) { create(:service_desk_custom_email_credential, project: project) }
+
+ let!(:verification) { create(:service_desk_custom_email_verification, project: project) }
+
+ subject(:setting) { build_stubbed(:service_desk_setting, project: project) }
+
+ before do
+ project.reset
+ end
+
+ context 'when custom email verification started' do
+ it 'is not valid' do
+ is_expected.not_to be_valid
+ expect(setting.errors[:custom_email_enabled].join).to include(expected_error_part)
+ end
+ end
+
+ context 'when custom email verification has been finished' do
+ before do
+ verification.mark_as_finished!
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
end
context 'when custom_email_enabled is true' do
before do
# Test without ServiceDesk::CustomEmailVerification for simplicity
- subject.custom_email_enabled = true
+ setting.custom_email_enabled = true
end
it { is_expected.to validate_presence_of(:custom_email) }
@@ -66,13 +105,13 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
describe '#custom_email_address_for_verification' do
it 'returns nil' do
- expect(subject.custom_email_address_for_verification).to be_nil
+ expect(setting.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')
+ setting.custom_email = 'support@example.com'
+ expect(setting.custom_email_address_for_verification).to eq('support+verify@example.com')
end
end
end
@@ -114,6 +153,8 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
end
describe 'associations' do
+ let(:project) { build(:project) }
+ let(:verification) { build(:service_desk_custom_email_verification) }
let(:custom_email_settings) do
build_stubbed(
:service_desk_setting,
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 8669db4af16..2d6a674d3ce 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -446,7 +446,7 @@ RSpec.describe Todo do
end
specify do
- expect(Todo.count_grouped_by_user_id_and_state).to eq({ [user1.id, "done"] => 1, [user1.id, "pending"] => 2, [user2.id, "pending"] => 1 })
+ expect(described_class.count_grouped_by_user_id_and_state).to eq({ [user1.id, "done"] => 1, [user1.id, "pending"] => 2, [user2.id, "pending"] => 1 })
end
end
diff --git a/spec/models/user_custom_attribute_spec.rb b/spec/models/user_custom_attribute_spec.rb
index 934956926f0..7d3806fcdfa 100644
--- a/spec/models/user_custom_attribute_spec.rb
+++ b/spec/models/user_custom_attribute_spec.rb
@@ -22,19 +22,19 @@ RSpec.describe UserCustomAttribute, feature_category: :user_profile do
let(:custom_attribute) { create(:user_custom_attribute, key: 'blocked_at', value: blocked_at, user_id: user.id) }
describe '.by_user_id' do
- subject { UserCustomAttribute.by_user_id(user.id) }
+ subject { described_class.by_user_id(user.id) }
it { is_expected.to match_array([custom_attribute]) }
end
describe '.by_updated_at' do
- subject { UserCustomAttribute.by_updated_at(Date.today.all_day) }
+ subject { described_class.by_updated_at(Date.today.all_day) }
it { is_expected.to match_array([custom_attribute]) }
end
describe '.by_key' do
- subject { UserCustomAttribute.by_key('blocked_at') }
+ subject { described_class.by_key('blocked_at') }
it { is_expected.to match_array([custom_attribute]) }
end
@@ -44,7 +44,7 @@ RSpec.describe UserCustomAttribute, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
let(:abuse_report) { create(:abuse_report, user: user) }
- subject { UserCustomAttribute.set_banned_by_abuse_report(abuse_report) }
+ subject { described_class.set_banned_by_abuse_report(abuse_report) }
it 'adds the abuse report ID to user custom attributes' do
subject
@@ -66,7 +66,7 @@ RSpec.describe UserCustomAttribute, feature_category: :user_profile do
end
describe '#upsert_custom_attributes' do
- subject { UserCustomAttribute.upsert_custom_attributes(custom_attributes) }
+ subject { described_class.upsert_custom_attributes(custom_attributes) }
let_it_be_with_reload(:user) { create(:user) }
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 17899012aaa..729635b5a27 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -49,8 +49,7 @@ RSpec.describe UserPreference, feature_category: :user_profile do
end
describe 'pass_user_identities_to_ci_jwt' do
- it { is_expected.to allow_value(true).for(:pass_user_identities_to_ci_jwt) }
- it { is_expected.to allow_value(false).for(:pass_user_identities_to_ci_jwt) }
+ it { is_expected.to allow_value(true, false).for(:pass_user_identities_to_ci_jwt) }
it { is_expected.not_to allow_value(nil).for(:pass_user_identities_to_ci_jwt) }
it { is_expected.not_to allow_value("").for(:pass_user_identities_to_ci_jwt) }
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 690c0be3b7a..059cbac638b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -137,6 +137,7 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_one(:banned_user) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:members) }
+ it { is_expected.to have_many(:member_namespaces) }
it { is_expected.to have_many(:project_members) }
it { is_expected.to have_many(:group_members) }
it { is_expected.to have_many(:groups) }
@@ -186,6 +187,15 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') }
it do
+ is_expected.to have_many(:organization_users).class_name('Organizations::OrganizationUser').inverse_of(:user)
+ end
+
+ it do
+ is_expected.to have_many(:organizations)
+ .through(:organization_users).class_name('Organizations::Organization').inverse_of(:users)
+ end
+
+ it do
is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee)
end
@@ -358,13 +368,13 @@ RSpec.describe User, feature_category: :user_profile do
context 'when password is updated' do
context 'default behaviour' do
it 'enqueues the `password changed` email' do
- user.password = User.random_password
+ user.password = described_class.random_password
expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change)
end
it 'does not enqueue the `admin changed your password` email' do
- user.password = User.random_password
+ user.password = described_class.random_password
expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
end
@@ -372,21 +382,21 @@ RSpec.describe User, feature_category: :user_profile do
context '`admin changed your password` email' do
it 'is enqueued only when explicitly allowed' do
- user.password = User.random_password
+ user.password = described_class.random_password
user.send_only_admin_changed_your_password_notification!
expect { user.save! }.to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
end
it '`password changed` email is not enqueued if it is explicitly allowed' do
- user.password = User.random_password
+ user.password = described_class.random_password
user.send_only_admin_changed_your_password_notification!
expect { user.save! }.not_to have_enqueued_mail(DeviseMailer, :password_changed)
end
it 'is not enqueued if sending notifications on password updates is turned off as per Devise config' do
- user.password = User.random_password
+ user.password = described_class.random_password
user.send_only_admin_changed_your_password_notification!
allow(Devise).to receive(:send_password_change_notification).and_return(false)
@@ -412,7 +422,7 @@ RSpec.describe User, feature_category: :user_profile do
context 'when email is changed to another before performing the job that sends confirmation instructions for previous email change request' do
it "mentions the recipient's email in the message body", :aggregate_failures do
- same_user = User.find(user.id)
+ same_user = described_class.find(user.id)
same_user.update!(email: unconfirmed_email)
user.update!(email: another_unconfirmed_email)
@@ -537,7 +547,7 @@ RSpec.describe User, feature_category: :user_profile do
end
it 'does not check if the user is a new record' do
- user = User.new(username: 'newuser')
+ user = described_class.new(username: 'newuser')
expect(user.new_record?).to eq(true)
expect(user).not_to receive(:namespace_move_dir_allowed)
@@ -1171,7 +1181,7 @@ RSpec.describe User, feature_category: :user_profile do
let(:random_password) { described_class.random_password }
before do
- expect(User).to receive(:password_length).and_return(88..128)
+ expect(described_class).to receive(:password_length).and_return(88..128)
end
context 'length' do
@@ -1405,7 +1415,7 @@ RSpec.describe User, feature_category: :user_profile do
context 'strip attributes' do
context 'name' do
- let(:user) { User.new(name: ' John Smith ') }
+ let(:user) { described_class.new(name: ' John Smith ') }
it 'strips whitespaces on validation' do
expect { user.valid? }.to change { user.name }.to('John Smith')
@@ -1677,7 +1687,7 @@ RSpec.describe User, feature_category: :user_profile do
end
it 'returns the correct highest role' do
- users = User.includes(:user_highest_role).where(id: [user.id, another_user.id])
+ users = described_class.includes(:user_highest_role).where(id: [user.id, another_user.id])
expect(users.collect { |u| [u.id, u.highest_role] }).to contain_exactly(
[user.id, Gitlab::Access::MAINTAINER],
@@ -2054,7 +2064,7 @@ RSpec.describe User, feature_category: :user_profile do
describe '#generate_password' do
it 'does not generate password by default' do
- password = User.random_password
+ password = described_class.random_password
user = create(:user, password: password)
expect(user.password).to eq(password)
@@ -2741,9 +2751,9 @@ RSpec.describe User, feature_category: :user_profile do
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }
it 'returns up to the ten most recently active instance admins' do
- active_admins_in_recent_sign_in_desc_order = User.admins.active.order_recent_sign_in.limit(10)
+ active_admins_in_recent_sign_in_desc_order = described_class.admins.active.order_recent_sign_in.limit(10)
- expect(User.instance_access_request_approvers_to_be_notified).to eq(active_admins_in_recent_sign_in_desc_order)
+ expect(described_class.instance_access_request_approvers_to_be_notified).to eq(active_admins_in_recent_sign_in_desc_order)
end
end
@@ -2950,56 +2960,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- describe '#spammer?' do
- let_it_be(:user) { create(:user) }
-
- context 'when the user is a spammer' do
- before do
- allow(user).to receive(:spam_score).and_return(0.9)
- end
-
- it 'classifies the user as a spammer' do
- expect(user).to be_spammer
- end
- end
-
- context 'when the user is not a spammer' do
- before do
- allow(user).to receive(:spam_score).and_return(0.1)
- end
-
- it 'does not classify the user as a spammer' do
- expect(user).not_to be_spammer
- end
- end
- end
-
- describe '#spam_score' do
- let_it_be(:user) { create(:user) }
-
- context 'when the user is a spammer' do
- before do
- create(:abuse_trust_score, user: user, score: 0.8)
- create(:abuse_trust_score, user: user, score: 0.9)
- end
-
- it 'returns the expected score' do
- expect(user.spam_score).to be_within(0.01).of(0.85)
- end
- end
-
- context 'when the user is not a spammer' do
- before do
- create(:abuse_trust_score, user: user, score: 0.1)
- create(:abuse_trust_score, user: user, score: 0.0)
- end
-
- it 'returns the expected score' do
- expect(user.spam_score).to be_within(0.01).of(0.05)
- end
- end
- end
-
describe '.find_for_database_authentication' do
it 'strips whitespace from login' do
user = create(:user)
@@ -4497,7 +4457,7 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to include(group) }
it 'avoids N+1 queries' do
- fresh_user = User.find(user.id)
+ fresh_user = described_class.find(user.id)
control_count = ActiveRecord::QueryRecorder.new do
fresh_user.solo_owned_groups
end.count
@@ -6145,7 +6105,9 @@ RSpec.describe User, feature_category: :user_profile do
context 'when the user is a spammer' do
before do
- allow(user).to receive(:spammer?).and_return(true)
+ user_scores = Abuse::UserTrustScore.new(user)
+ allow(Abuse::UserTrustScore).to receive(:new).and_return(user_scores)
+ allow(user_scores).to receive(:spammer?).and_return(true)
end
context 'when the user account is less than 7 days old' do
@@ -6154,7 +6116,7 @@ RSpec.describe User, feature_category: :user_profile do
it 'creates an abuse report with the correct data' do
expect { subject }.to change { AbuseReport.count }.from(0).to(1)
expect(AbuseReport.last.attributes).to include({
- reporter_id: User.security_bot.id,
+ reporter_id: described_class.security_bot.id,
user_id: user.id,
category: "spam",
message: 'Potential spammer account deletion'
@@ -6175,7 +6137,7 @@ RSpec.describe User, feature_category: :user_profile do
end
context 'when there is an existing abuse report' do
- let!(:abuse_report) { create(:abuse_report, user: user, reporter: User.security_bot, message: 'Existing') }
+ let!(:abuse_report) { create(:abuse_report, user: user, reporter: described_class.security_bot, message: 'Existing') }
it 'updates the abuse report' do
subject
@@ -6230,6 +6192,29 @@ RSpec.describe User, feature_category: :user_profile do
expect { user.delete_async(deleted_by: deleted_by) }.not_to change { user.note }
end
end
+
+ describe '#allow_possible_spam?' do
+ context 'when no custom attribute is set' do
+ it 'is false' do
+ expect(user.allow_possible_spam?).to be_falsey
+ end
+ end
+
+ context 'when the custom attribute is set' do
+ before do
+ user.custom_attributes.upsert_custom_attributes(
+ [{
+ user_id: user.id,
+ key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM,
+ value: "test"
+ }])
+ end
+
+ it '#allow_possible_spam? is true' do
+ expect(user.allow_possible_spam?).to be_truthy
+ end
+ end
+ end
end
end
@@ -6414,9 +6399,8 @@ RSpec.describe User, feature_category: :user_profile do
describe '#required_terms_not_accepted?' do
let(:user) { build(:user) }
- let(:project_bot) { create(:user, :project_bot) }
- subject { user.required_terms_not_accepted? }
+ subject(:required_terms_not_accepted) { user.required_terms_not_accepted? }
context 'when terms are not enforced' do
it { is_expected.to be_falsey }
@@ -6428,17 +6412,25 @@ RSpec.describe User, feature_category: :user_profile do
end
it 'is not accepted by the user' do
- expect(subject).to be_truthy
+ expect(required_terms_not_accepted).to be_truthy
end
it 'is accepted by the user' do
accept_terms(user)
- expect(subject).to be_falsey
+ expect(required_terms_not_accepted).to be_falsey
end
- it 'auto accepts the term for project bots' do
- expect(project_bot.required_terms_not_accepted?).to be_falsey
+ context "with bot users" do
+ %i[project_bot service_account security_policy_bot].each do |user_type|
+ context "when user is #{user_type}" do
+ let(:user) { build(:user, user_type) }
+
+ it 'auto accepts the terms' do
+ expect(required_terms_not_accepted).to be_falsey
+ end
+ end
+ end
end
end
end
@@ -7690,7 +7682,7 @@ RSpec.describe User, feature_category: :user_profile do
context 'when confirmation period is expired' do
before do
- travel_to(User.allow_unconfirmed_access_for.from_now + 1.day)
+ travel_to(described_class.allow_unconfirmed_access_for.from_now + 1.day)
end
it { is_expected.to be(true) }
@@ -8110,70 +8102,4 @@ RSpec.describe User, feature_category: :user_profile do
end
end
end
-
- describe '#telesign_score' do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
-
- context 'when the user has a telesign risk score' do
- before do
- create(:abuse_trust_score, user: user1, score: 12.0, source: :telesign)
- create(:abuse_trust_score, user: user1, score: 24.0, source: :telesign)
- end
-
- it 'returns the latest score' do
- expect(user1.telesign_score).to be(24.0)
- end
- end
-
- context 'when the user does not have a telesign risk score' do
- it 'defaults to zero' do
- expect(user2.telesign_score).to be(0.0)
- end
- end
- end
-
- describe '#arkose_global_score' do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
-
- context 'when the user has an arkose global risk score' do
- before do
- create(:abuse_trust_score, user: user1, score: 12.0, source: :arkose_global_score)
- create(:abuse_trust_score, user: user1, score: 24.0, source: :arkose_global_score)
- end
-
- it 'returns the latest score' do
- expect(user1.arkose_global_score).to be(24.0)
- end
- end
-
- context 'when the user does not have an arkose global risk score' do
- it 'defaults to zero' do
- expect(user2.arkose_global_score).to be(0.0)
- end
- end
- end
-
- describe '#arkose_custom_score' do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
-
- context 'when the user has an arkose custom risk score' do
- before do
- create(:abuse_trust_score, user: user1, score: 12.0, source: :arkose_custom_score)
- create(:abuse_trust_score, user: user1, score: 24.0, source: :arkose_custom_score)
- end
-
- it 'returns the latest score' do
- expect(user1.arkose_custom_score).to be(24.0)
- end
- end
-
- context 'when the user does not have an arkose custom risk score' do
- it 'defaults to zero' do
- expect(user2.arkose_custom_score).to be(0.0)
- end
- end
- end
end
diff --git a/spec/models/users/merge_request_interaction_spec.rb b/spec/models/users/merge_request_interaction_spec.rb
index 0b1888bd9a6..c7ffb62e5f1 100644
--- a/spec/models/users/merge_request_interaction_spec.rb
+++ b/spec/models/users/merge_request_interaction_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ::Users::MergeRequestInteraction do
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
subject(:interaction) do
- ::Users::MergeRequestInteraction.new(user: user, merge_request: merge_request.reset)
+ described_class.new(user: user, merge_request: merge_request.reset)
end
describe 'declarative policy delegation' do
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index add9bd18755..d1ca9ff5125 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe UsersStatistics do
context 'when unsuccessful' do
it 'raises an ActiveRecord::RecordInvalid exception' do
- allow(UsersStatistics).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
+ allow(described_class).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
expect { described_class.create_current_stats! }.to raise_error(ActiveRecord::RecordInvalid)
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index c30e79f79ce..93e7ecd7646 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -33,18 +33,18 @@ RSpec.describe WikiDirectory do
expect(entries).to match(
[
- a_kind_of(WikiDirectory).and(
+ a_kind_of(described_class).and(
having_attributes(
slug: 'Home', entries: [homechild]
)
),
toplevel1,
- a_kind_of(WikiDirectory).and(
+ a_kind_of(described_class).and(
having_attributes(
slug: 'parent1', entries: [
child1,
child2,
- a_kind_of(WikiDirectory).and(
+ a_kind_of(described_class).and(
having_attributes(
slug: 'parent1/subparent',
entries: [grandchild1, grandchild2]
@@ -53,7 +53,7 @@ RSpec.describe WikiDirectory do
]
)
),
- a_kind_of(WikiDirectory).and(
+ a_kind_of(described_class).and(
having_attributes(
slug: 'parent2',
entries: [child3]
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index e0ec54fd5ff..7963c0898b3 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -87,6 +87,14 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#todoable_target_type_name' do
+ it 'returns correct target name' do
+ work_item = build(:work_item)
+
+ expect(work_item.todoable_target_type_name).to contain_exactly('Issue', 'WorkItem')
+ end
+ end
+
describe '#widgets' do
subject { build(:work_item).widgets }
@@ -176,17 +184,32 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
is_expected.not_to include(:due, :remove_due_date)
end
end
+
+ context 'when work item supports the current user todos widget' do
+ it 'returns todos related quick action commands' do
+ is_expected.to include(:todo, :done)
+ end
+ end
+
+ context 'when work item does not support current user todos widget' do
+ let(:work_item) { build(:work_item, :task) }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:current_user_todos).update!(disabled: true)
+ end
+
+ it 'omits todos related quick action commands' do
+ is_expected.not_to include(:todo, :done)
+ end
+ end
end
describe 'transform_quick_action_params' do
+ let(:command_params) { { title: 'bar', assignee_ids: ['foo'] } }
let(:work_item) { build(:work_item, :task) }
- subject(:transformed_params) do
- work_item.transform_quick_action_params({
- title: 'bar',
- assignee_ids: ['foo']
- })
- end
+ subject(:transformed_params) { work_item.transform_quick_action_params(command_params) }
it 'correctly separates widget params from regular params' do
expect(transformed_params).to eq({
@@ -200,6 +223,30 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
}
})
end
+
+ context 'with current user todos widget' do
+ let(:command_params) { { title: 'bar', todo_event: param } }
+
+ where(:param, :expected) do
+ 'done' | 'mark_as_done'
+ 'add' | 'add'
+ end
+
+ with_them do
+ it 'correctly transform todo_event param' do
+ expect(transformed_params).to eq({
+ common: {
+ title: 'bar'
+ },
+ widgets: {
+ current_user_todos_widget: {
+ action: expected
+ }
+ }
+ })
+ end
+ end
+ end
end
describe 'callbacks' do
@@ -212,11 +259,13 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
create(:work_item)
end
- it_behaves_like 'issue_edit snowplow tracking' do
+ it_behaves_like 'internal event tracking' do
let(:work_item) { create(:work_item) }
- let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CREATED }
+ let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CREATED }
let(:project) { work_item.project }
let(:user) { work_item.author }
+ let(:namespace) { project.namespace }
+
subject(:service_action) { work_item }
end
end
@@ -255,22 +304,6 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
describe 'validations' do
subject { work_item.valid? }
- describe 'issue_type' do
- let(:work_item) { build(:work_item, issue_type: issue_type) }
-
- context 'when a valid type' do
- let(:issue_type) { :issue }
-
- it { is_expected.to eq(true) }
- end
-
- context 'empty type' do
- let(:issue_type) { nil }
-
- it { is_expected.to eq(false) }
- end
- end
-
describe 'confidentiality' do
let_it_be(:project) { create(:project) }
diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb
index f1aa81f46d2..d7f87da1965 100644
--- a/spec/models/work_items/parent_link_spec.rb
+++ b/spec/models/work_items/parent_link_spec.rb
@@ -51,8 +51,8 @@ RSpec.describe WorkItems::ParentLink, feature_category: :portfolio_management do
it 'validates if child can be added to the parent' do
parent_type = WorkItems::Type.default_by_type(parent_type_sym)
child_type = WorkItems::Type.default_by_type(child_type_sym)
- parent = build(:work_item, issue_type: parent_type_sym, work_item_type: parent_type, project: project)
- child = build(:work_item, issue_type: child_type_sym, work_item_type: child_type, project: project)
+ parent = build(:work_item, work_item_type: parent_type, project: project)
+ child = build(:work_item, work_item_type: child_type, project: project)
link = build(:parent_link, work_item: child, work_item_parent: parent)
expect(link.valid?).to eq(is_valid)
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index e5c88634b26..f5806c296ac 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -49,10 +49,10 @@ RSpec.describe WorkItems::Type do
it 'deletes type but not unrelated issues' do
type = create(:work_item_type)
- expect(WorkItems::Type.count).to eq(8)
+ expect(described_class.count).to eq(8)
expect { type.destroy! }.not_to change(Issue, :count)
- expect(WorkItems::Type.count).to eq(7)
+ expect(described_class.count).to eq(7)
end
end
diff --git a/spec/models/work_items/widgets/base_spec.rb b/spec/models/work_items/widgets/base_spec.rb
index 9b4b4d9e98f..29b54a706c2 100644
--- a/spec/models/work_items/widgets/base_spec.rb
+++ b/spec/models/work_items/widgets/base_spec.rb
@@ -16,4 +16,10 @@ RSpec.describe WorkItems::Widgets::Base do
it { is_expected.to eq(:base) }
end
+
+ describe '.process_quick_action_param' do
+ subject { described_class.process_quick_action_param(:label_ids, [1, 2]) }
+
+ it { is_expected.to eq({ label_ids: [1, 2] }) }
+ end
end
diff --git a/spec/models/work_items/widgets/current_user_todos_spec.rb b/spec/models/work_items/widgets/current_user_todos_spec.rb
new file mode 100644
index 00000000000..9fdf28beada
--- /dev/null
+++ b/spec/models/work_items/widgets/current_user_todos_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::CurrentUserTodos, feature_category: :team_planning do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:work_item) { create(:work_item, :issue, project: project, milestone: milestone) }
+
+ describe '.type' do
+ subject { described_class.type }
+
+ it { is_expected.to eq(:current_user_todos) }
+ end
+
+ describe '#type' do
+ subject { described_class.new(work_item).type }
+
+ it { is_expected.to eq(:current_user_todos) }
+ end
+
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to contain_exactly(:todo_event) }
+ end
+
+ describe '.quick_action_commands' do
+ subject { described_class.quick_action_commands }
+
+ it { is_expected.to contain_exactly(:todo, :done) }
+ end
+
+ describe '.process_quick_action_param' do
+ subject { described_class.process_quick_action_param(param_name, param_value) }
+
+ context 'when quick action param is todo_event' do
+ let(:param_name) { :todo_event }
+
+ context 'when param value is `done`' do
+ let(:param_value) { 'done' }
+
+ it { is_expected.to eq({ action: 'mark_as_done' }) }
+ end
+
+ context 'when param value is `add`' do
+ let(:param_value) { 'add' }
+
+ it { is_expected.to eq({ action: 'add' }) }
+ end
+ end
+
+ context 'when quick action param is not todo_event' do
+ let(:param_name) { :foo }
+ let(:param_value) { 'foo' }
+
+ it { is_expected.to eq({ foo: 'foo' }) }
+ end
+ end
+end
diff --git a/spec/models/work_items/widgets/milestone_spec.rb b/spec/models/work_items/widgets/milestone_spec.rb
index 7b2d661df29..385614984fe 100644
--- a/spec/models/work_items/widgets/milestone_spec.rb
+++ b/spec/models/work_items/widgets/milestone_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe WorkItems::Widgets::Milestone do
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
- let_it_be(:work_item) { create(:work_item, :issue, project: project, milestone: milestone) }
+ let_it_be(:work_item) { create(:work_item, project: project, milestone: milestone) }
describe '.type' do
subject { described_class.type }
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index dce97fab252..4fafe392aac 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -694,59 +694,5 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:create_instance_runner) }
end
-
- context 'create_runner_workflow_for_admin flag disabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_admin: false)
- end
-
- context 'admin' do
- let(:current_user) { admin_user }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
-
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
- end
-
- context 'with project_bot' do
- let(:current_user) { project_bot }
-
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
-
- context 'with migration_bot' do
- let(:current_user) { migration_bot }
-
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
-
- context 'with security_bot' do
- let(:current_user) { security_bot }
-
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
-
- context 'with llm_bot' do
- let(:current_user) { llm_bot }
-
- it { is_expected.to be_disallowed(:create_instance_runners) }
- end
-
- context 'with regular user' do
- let(:current_user) { user }
-
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
-
- context 'with anonymous' do
- let(:current_user) { nil }
-
- it { is_expected.to be_disallowed(:create_instance_runner) }
- end
- end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index fcde094939a..89f083a69d6 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1483,155 +1483,81 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end
end
- context 'create_runner_workflow_for_namespace flag enabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: [group])
- end
+ context 'admin' do
+ let(:current_user) { admin }
- context 'admin' do
- let(:current_user) { admin }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:create_runner) }
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:create_runner) }
+ context 'with specific group runner registration disabled' do
+ before do
+ group.runner_registration_enabled = false
+ end
- context 'with specific group runner registration disabled' do
- before do
- group.runner_registration_enabled = false
- end
+ it { is_expected.to be_allowed(:create_runner) }
+ end
- it { is_expected.to be_allowed(:create_runner) }
+ context 'with group runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['project'])
+ group.runner_registration_enabled = runner_registration_enabled
end
- context 'with group runner registration disabled' do
- before do
- stub_application_setting(valid_runner_registrars: ['project'])
- group.runner_registration_enabled = runner_registration_enabled
- end
+ context 'with specific group runner registration enabled' do
+ let(:runner_registration_enabled) { true }
- context 'with specific group runner registration enabled' do
- let(:runner_registration_enabled) { true }
-
- it { is_expected.to be_allowed(:create_runner) }
- end
+ it { is_expected.to be_allowed(:create_runner) }
+ end
- context 'with specific group runner registration disabled' do
- let(:runner_registration_enabled) { false }
+ context 'with specific group runner registration disabled' do
+ let(:runner_registration_enabled) { false }
- it { is_expected.to be_allowed(:create_runner) }
- end
+ it { is_expected.to be_allowed(:create_runner) }
end
end
-
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:create_runner) }
- end
- end
-
- context 'with owner' do
- let(:current_user) { owner }
-
- it { is_expected.to be_allowed(:create_runner) }
-
- it_behaves_like 'disallowed when group runner registration disabled'
- end
-
- context 'with maintainer' do
- let(:current_user) { maintainer }
-
- it { is_expected.to be_disallowed(:create_runner) }
end
- context 'with reporter' do
- let(:current_user) { reporter }
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with guest' do
- let(:current_user) { guest }
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with developer' do
- let(:current_user) { developer }
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with anonymous' do
- let(:current_user) { nil }
-
+ context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
- context 'with create_runner_workflow_for_namespace flag disabled' do
- before do
- 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 }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_disallowed(:create_runner) }
-
- context 'with specific group runner registration disabled' do
- before do
- group.runner_registration_enabled = false
- end
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- it_behaves_like 'disallowed when group runner registration disabled'
- end
-
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:create_runner) }
- end
- end
-
- context 'with owner' do
- let(:current_user) { owner }
+ context 'with owner' do
+ let(:current_user) { owner }
- it { is_expected.to be_disallowed(:create_runner) }
+ it { is_expected.to be_allowed(:create_runner) }
- it_behaves_like 'disallowed when group runner registration disabled'
- end
+ it_behaves_like 'disallowed when group runner registration disabled'
+ end
- context 'with maintainer' do
- let(:current_user) { maintainer }
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with reporter' do
- let(:current_user) { reporter }
+ context 'with reporter' do
+ let(:current_user) { reporter }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with guest' do
- let(:current_user) { guest }
+ context 'with guest' do
+ let(:current_user) { guest }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with developer' do
- let(:current_user) { developer }
+ context 'with developer' do
+ let(:current_user) { developer }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with anonymous' do
- let(:current_user) { nil }
+ context 'with anonymous' do
+ let(:current_user) { nil }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
end
end
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index c21e1244402..285f52956eb 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -462,6 +462,37 @@ RSpec.describe MergeRequestPolicy do
end
end
+ context 'when enabling generate diff summary permission' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:mr) { create(:merge_request, target_project: project, source_project: project) }
+ let_it_be(:user) { create(:user) }
+ let(:policy) { permissions(user, mr) }
+
+ context 'when can read_merge_request' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'allows to generate_diff_summary' do
+ expect(policy).to be_allowed(:generate_diff_summary)
+ end
+ end
+
+ context 'when can not read_merge_request' do
+ it 'does not allow to generate_diff_summary' do
+ expect(policy).not_to be_allowed(:generate_diff_summary)
+ end
+
+ context 'and when is the LLM bot' do
+ let(:user) { create(:user, :llm_bot) }
+
+ it 'allows to generate_diff_summary' do
+ expect(policy).to be_allowed(:generate_diff_summary)
+ end
+ end
+ end
+ end
+
context 'when the author of the merge request is banned', feature_category: :insider_threat do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index b2191e6925d..de300f933f6 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -271,7 +271,7 @@ RSpec.describe NotePolicy, feature_category: :team_planning do
end
context 'when noteable is issue' do
- let(:noteable) { create(:work_item, :issue, project: project) }
+ let(:noteable) { create(:work_item, project: project) }
let(:note) { create(:note, system: true, noteable: noteable, author: user, project: project) }
it_behaves_like 'user can read the note'
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index ee8d811971a..602b7148d0e 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -2879,42 +2879,10 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
describe 'create_runner' do
- context 'create_runner_workflow_for_namespace flag enabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
- end
-
- context 'admin' do
- let(:current_user) { admin }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:create_runner) }
-
- context 'with project runner registration disabled' do
- before do
- stub_application_setting(valid_runner_registrars: ['group'])
- end
-
- it { is_expected.to be_allowed(:create_runner) }
- end
-
- context 'with specific project runner registration disabled' do
- before do
- project.update!(runner_registration_enabled: false)
- end
-
- it { is_expected.to be_allowed(:create_runner) }
- end
- end
-
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:create_runner) }
- end
- end
-
- context 'with owner' do
- let(:current_user) { owner }
+ context 'admin' do
+ let(:current_user) { admin }
+ context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:create_runner) }
context 'with project runner registration disabled' do
@@ -2922,7 +2890,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
stub_application_setting(valid_runner_registrars: ['group'])
end
- it { is_expected.to be_disallowed(:create_runner) }
+ it { is_expected.to be_allowed(:create_runner) }
end
context 'with specific project runner registration disabled' do
@@ -2930,125 +2898,65 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
project.update!(runner_registration_enabled: false)
end
- it { is_expected.to be_disallowed(:create_runner) }
+ it { is_expected.to be_allowed(:create_runner) }
end
end
- context 'with maintainer' do
- let(:current_user) { maintainer }
-
- it { is_expected.to be_allowed(:create_runner) }
- end
-
- context 'with reporter' do
- let(:current_user) { reporter }
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with guest' do
- let(:current_user) { guest }
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with developer' do
- let(:current_user) { developer }
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with anonymous' do
- let(:current_user) { nil }
-
+ context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_runner) }
end
end
- context 'create_runner_workflow_for_namespace flag disabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: [group])
- end
-
- context 'admin' do
- let(:current_user) { admin }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_disallowed(:create_runner) }
-
- context 'with project runner registration disabled' do
- before do
- stub_application_setting(valid_runner_registrars: ['group'])
- end
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
-
- context 'with specific project runner registration disabled' do
- before do
- project.update!(runner_registration_enabled: false)
- end
+ context 'with owner' do
+ let(:current_user) { owner }
- it { is_expected.to be_disallowed(:create_runner) }
- end
- end
+ it { is_expected.to be_allowed(:create_runner) }
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:create_runner) }
+ context 'with project runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
end
- end
-
- context 'with owner' do
- let(:current_user) { owner }
it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with project runner registration disabled' do
- before do
- stub_application_setting(valid_runner_registrars: ['group'])
- end
-
- it { is_expected.to be_disallowed(:create_runner) }
+ context 'with specific project runner registration disabled' do
+ before do
+ project.update!(runner_registration_enabled: false)
end
- context 'with specific project runner registration disabled' do
- before do
- project.update!(runner_registration_enabled: false)
- end
-
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
end
+ end
- context 'with maintainer' do
- let(:current_user) { maintainer }
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_allowed(:create_runner) }
+ end
- context 'with reporter' do
- let(:current_user) { reporter }
+ context 'with reporter' do
+ let(:current_user) { reporter }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with guest' do
- let(:current_user) { guest }
+ context 'with guest' do
+ let(:current_user) { guest }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with developer' do
- let(:current_user) { developer }
+ context 'with developer' do
+ let(:current_user) { developer }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
+ end
- context 'with anonymous' do
- let(:current_user) { nil }
+ context 'with anonymous' do
+ let(:current_user) { nil }
- it { is_expected.to be_disallowed(:create_runner) }
- end
+ it { is_expected.to be_disallowed(:create_runner) }
end
end
@@ -3309,6 +3217,57 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
+ describe ':write_model_experiments' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ff_ml_experiment_tracking, :current_user, :access_level, :allowed) do
+ false | ref(:owner) | Featurable::ENABLED | false
+ true | ref(:reporter) | Featurable::ENABLED | true
+ true | ref(:reporter) | Featurable::PRIVATE | true
+ true | ref(:reporter) | Featurable::DISABLED | false
+ true | ref(:guest) | Featurable::ENABLED | false
+ true | ref(:non_member) | Featurable::ENABLED | false
+ end
+ with_them do
+ before do
+ stub_feature_flags(ml_experiment_tracking: ff_ml_experiment_tracking)
+ project.project_feature.update!(model_experiments_access_level: access_level)
+ end
+
+ if params[:allowed]
+ it { is_expected.to be_allowed(:write_model_experiments) }
+ else
+ it { is_expected.not_to be_allowed(:write_model_experiments) }
+ end
+ end
+ end
+
+ describe 'when project is created and owned by a banned user' do
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:current_user) { guest }
+
+ before do
+ allow(project).to receive(:created_and_owned_by_banned_user?).and_return(true)
+ end
+
+ it { expect_disallowed(:read_project) }
+
+ context 'when current user is an admin', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it { expect_allowed(:read_project) }
+ end
+
+ context 'when hide_projects_of_banned_users FF is disabled' do
+ before do
+ stub_feature_flags(hide_projects_of_banned_users: false)
+ end
+
+ it { expect_allowed(:read_project) }
+ end
+ end
+
private
def project_subject(project_type)
diff --git a/spec/presenters/alert_management/alert_presenter_spec.rb b/spec/presenters/alert_management/alert_presenter_spec.rb
index fe228f174fe..eedb2e07fb6 100644
--- a/spec/presenters/alert_management/alert_presenter_spec.rb
+++ b/spec/presenters/alert_management/alert_presenter_spec.rb
@@ -91,24 +91,6 @@ RSpec.describe AlertManagement::AlertPresenter do
)
end
end
-
- context 'with metrics_dashboard_url' do
- before do
- allow(alert.parsed_payload).to receive(:metrics_dashboard_url).and_return('https://gitlab.com/metrics')
- end
-
- it do
- is_expected.to eq(
- <<~MARKDOWN.chomp
- **Start time:** #{presenter.start_time}#{markdown_line_break}
- **Severity:** #{presenter.severity}#{markdown_line_break}
- **GitLab alert:** #{alert_url}
-
- [](https://gitlab.com/metrics)
- MARKDOWN
- )
- end
- end
end
describe '#start_time' do
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index e776716bd2d..150c7bd5f3e 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -7,28 +7,52 @@ RSpec.describe BlobPresenter do
let_it_be(:user) { project.first_owner }
let(:repository) { project.repository }
- let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') }
+ let(:blob) { repository.blob_at(ref, path) }
+ let(:ref) { 'HEAD' }
+ let(:path) { 'files/ruby/regex.rb' }
subject(:presenter) { described_class.new(blob, current_user: user) }
describe '#web_url' do
- it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{ref}/#{path}") }
end
describe '#web_path' do
- it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{ref}/#{path}") }
end
describe '#edit_blob_path' do
- it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{ref}/#{path}") }
end
describe '#raw_path' do
- it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{ref}/#{path}") }
end
describe '#replace_path' do
- it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{ref}/#{path}") }
+ end
+
+ shared_examples_for '#can_current_user_push_to_branch?' do
+ let(:branch_exists) { true }
+
+ before do
+ allow(project.repository).to receive(:branch_exists?).with(blob.commit_id).and_return(branch_exists)
+ end
+
+ it { expect(presenter.can_current_user_push_to_branch?).to eq(true) }
+
+ context 'current_user is nil' do
+ let(:user) { nil }
+
+ it { expect(presenter.can_current_user_push_to_branch?).to eq(false) }
+ end
+
+ context 'branch does not exist' do
+ let(:branch_exists) { false }
+
+ it { expect(presenter.can_current_user_push_to_branch?).to eq(false) }
+ end
end
context 'when blob has ref_type' do
@@ -37,46 +61,67 @@ RSpec.describe BlobPresenter do
end
describe '#web_url' do
- it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
+ it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{ref}/#{path}?ref_type=heads") }
end
describe '#web_path' do
- it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
+ it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{ref}/#{path}?ref_type=heads") }
end
describe '#edit_blob_path' do
- it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
+ it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{ref}/#{path}?ref_type=heads") }
end
describe '#raw_path' do
- it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
+ it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{ref}/#{path}?ref_type=heads") }
end
describe '#replace_path' do
- it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{blob.commit_id}/#{blob.path}?ref_type=heads") }
+ it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/update/#{ref}/#{path}?ref_type=heads") }
end
+
+ it_behaves_like '#can_current_user_push_to_branch?'
end
- describe '#can_current_user_push_to_branch' do
- let(:branch_exists) { true }
+ describe '#can_modify_blob?' do
+ context 'when blob is store externally' do
+ before do
+ allow(blob).to receive(:stored_externally?).and_return(true)
+ end
- before do
- allow(project.repository).to receive(:branch_exists?).with(blob.commit_id).and_return(branch_exists)
+ it { expect(presenter.can_modify_blob?).to be_falsey }
end
- it { expect(presenter.can_current_user_push_to_branch?).to eq(true) }
+ context 'when the user cannot edit the tree' do
+ before do
+ allow(presenter).to receive(:can_edit_tree?).with(project, ref).and_return(false)
+ end
- context 'current_user is nil' do
- let(:user) { nil }
+ it { expect(presenter.can_modify_blob?).to be_falsey }
+ end
- it { expect(presenter.can_current_user_push_to_branch?).to eq(false) }
+ context 'when ref is a branch' do
+ let(:ref) { 'feature' }
+
+ it { expect(presenter.can_modify_blob?).to be_truthy }
end
+ end
- context 'branch does not exist' do
- let(:branch_exists) { false }
+ describe '#can_current_user_push_to_branch?' do
+ context 'when ref is a branch' do
+ let(:ref) { 'feature' }
- it { expect(presenter.can_current_user_push_to_branch?).to eq(false) }
+ it 'delegates to UserAccess' do
+ allow_next_instance_of(Gitlab::UserAccess) do |instance|
+ expect(instance).to receive(:can_push_to_branch?).with(ref).and_call_original
+ end
+ expect(presenter.can_current_user_push_to_branch?).to be_truthy
+ end
end
+
+ it_behaves_like '#can_current_user_push_to_branch?'
+
+ it { expect(presenter.can_current_user_push_to_branch?).to be_falsey }
end
describe '#archived?' do
@@ -95,9 +140,10 @@ RSpec.describe BlobPresenter do
)
end
- let(:blob) { repository.blob_at('main', '.gitlab-ci.yml') }
+ let(:ref) { 'main' }
+ let(:path) { '.gitlab-ci.yml' }
- it { expect(presenter.pipeline_editor_path).to eq("/#{project.full_path}/-/ci/editor?branch_name=#{blob.commit_id}") }
+ it { expect(presenter.pipeline_editor_path).to eq("/#{project.full_path}/-/ci/editor?branch_name=#{ref}") }
end
end
@@ -114,7 +160,7 @@ RSpec.describe BlobPresenter do
context 'Gitpod enabled for application and user' do
describe '#gitpod_blob_url' do
- it { expect(presenter.gitpod_blob_url).to eq("#{gitpod_url}##{"http://localhost/#{project.full_path}/-/tree/#{blob.commit_id}/#{blob.path}"}") }
+ it { expect(presenter.gitpod_blob_url).to eq("#{gitpod_url}##{"http://localhost/#{project.full_path}/-/tree/#{ref}/#{path}"}") }
end
end
@@ -157,7 +203,7 @@ RSpec.describe BlobPresenter do
let!(:deployment) { create(:deployment, :success, environment: environment, project: project, sha: blob.commit_id) }
before do
- allow(project).to receive(:public_path_for_source_path).with(blob.path, blob.commit_id).and_return(blob.path)
+ allow(project).to receive(:public_path_for_source_path).with(path, blob.commit_id).and_return(path)
end
describe '#environment_formatted_external_url' do
@@ -165,7 +211,7 @@ RSpec.describe BlobPresenter do
end
describe '#environment_external_url_for_route_map' do
- it { expect(presenter.environment_external_url_for_route_map).to eq("#{external_url}/#{blob.path}") }
+ it { expect(presenter.environment_external_url_for_route_map).to eq("#{external_url}/#{path}") }
end
describe 'chooses the latest deployed environment for #environment_formatted_external_url and #environment_external_url_for_route_map' do
@@ -174,7 +220,7 @@ RSpec.describe BlobPresenter do
let!(:another_deployment) { create(:deployment, :success, environment: another_environment, project: project, sha: blob.commit_id) }
it { expect(presenter.environment_formatted_external_url).to eq("another.environment") }
- it { expect(presenter.environment_external_url_for_route_map).to eq("#{another_external_url}/#{blob.path}") }
+ it { expect(presenter.environment_external_url_for_route_map).to eq("#{another_external_url}/#{path}") }
end
end
@@ -219,7 +265,7 @@ RSpec.describe BlobPresenter do
end
describe '#code_navigation_path' do
- let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(blob.path) }
+ let(:code_navigation_path) { Gitlab::CodeNavigationPath.new(project, blob.commit_id).full_json_path_for(path) }
it { expect(presenter.code_navigation_path).to eq(code_navigation_path) }
end
@@ -232,11 +278,11 @@ RSpec.describe BlobPresenter do
let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) }
describe '#web_url' do
- it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{ref}/#{path}") }
end
describe '#web_path' do
- it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") }
+ it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{ref}/#{path}") }
end
end
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index cc68cdff7c1..fc13b377014 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -146,69 +146,6 @@ RSpec.describe Ci::PipelinePresenter do
end
end
- describe '#ref_text_legacy' do
- subject { presenter.ref_text_legacy }
-
- context 'when pipeline is detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.all_pipelines.last }
-
- it 'returns a correct ref text' do
- is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
- "with <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a>")
- end
- end
-
- context 'when pipeline is merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
- let(:pipeline) { merge_request.all_pipelines.last }
-
- it 'returns a correct ref text' do
- is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
- "with <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a> " \
- "into <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(merge_request.target_project, merge_request.target_branch)}\">#{merge_request.target_branch}</a>")
- end
- end
-
- context 'when pipeline is branch pipeline' do
- context 'when ref exists in the repository' do
- before do
- allow(pipeline).to receive(:ref_exists?) { true }
- end
-
- it 'returns a correct ref text' do
- is_expected.to eq("for <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(pipeline.project, pipeline.ref)}\">#{pipeline.ref}</a>")
- end
-
- context 'when ref contains malicious script' do
- let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) }
-
- it 'does not include the malicious script' do
- is_expected.not_to include("<script>alter('1')</script>")
- end
- end
- end
-
- context 'when ref does not exist in the repository' do
- before do
- allow(pipeline).to receive(:ref_exists?) { false }
- end
-
- it 'returns a correct ref text' do
- is_expected.to eq("for <span class=\"ref-name\">#{pipeline.ref}</span>")
- end
-
- context 'when ref contains malicious script' do
- let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) }
-
- it 'does not include the malicious script' do
- is_expected.not_to include("<script>alter('1')</script>")
- end
- end
- end
- end
- end
-
describe '#ref_text' do
subject { presenter.ref_text }
@@ -272,49 +209,6 @@ RSpec.describe Ci::PipelinePresenter do
end
end
- describe '#all_related_merge_request_text' do
- subject { presenter.all_related_merge_request_text }
-
- let_it_be(:mr_1) { create(:merge_request) }
- let_it_be(:mr_2) { create(:merge_request) }
-
- context 'with zero related merge requests (branch pipeline)' do
- it { is_expected.to eq('No related merge requests found.') }
- end
-
- context 'with one related merge request' do
- before do
- allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: mr_1.id))
- end
-
- it {
- is_expected.to eq("1 related merge request: " \
- "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_1)}\">#{mr_1.to_reference} #{mr_1.title}</a>")
- }
- end
-
- context 'with two related merge requests' do
- before do
- allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: [mr_1.id, mr_2.id]))
- end
-
- it {
- is_expected.to eq("2 related merge requests: " \
- "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_2)}\">#{mr_2.to_reference} #{mr_2.title}</a>, " \
- "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_1)}\">#{mr_1.to_reference} #{mr_1.title}</a>")
- }
-
- context 'with a limit passed' do
- subject { presenter.all_related_merge_request_text(limit: 1) }
-
- it {
- is_expected.to eq("2 related merge requests: " \
- "<a class=\"mr-iid\" href=\"#{merge_request_path(mr_2)}\">#{mr_2.to_reference} #{mr_2.title}</a>")
- }
- end
- end
- end
-
describe '#all_related_merge_requests' do
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
diff --git a/spec/presenters/ml/models_index_presenter_spec.rb b/spec/presenters/ml/models_index_presenter_spec.rb
new file mode 100644
index 00000000000..697b57a51c1
--- /dev/null
+++ b/spec/presenters/ml/models_index_presenter_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelsIndexPresenter, feature_category: :mlops do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:model1) { build_stubbed(:ml_model_package, project: project) }
+ let_it_be(:model2) { build_stubbed(:ml_model_package, project: project) }
+ let_it_be(:models) do
+ [model1, model2]
+ end
+
+ describe '#execute' do
+ subject { Gitlab::Json.parse(described_class.new(models).present)['models'] }
+
+ it 'presents models correctly' do
+ expected_models = [
+ {
+ 'name' => model1.name,
+ 'version' => model1.version,
+ 'path' => "/#{project.full_path}/-/packages/#{model1.id}"
+ },
+ {
+ 'name' => model2.name,
+ 'version' => model2.version,
+ 'path' => "/#{project.full_path}/-/packages/#{model2.id}"
+ }
+ ]
+
+ is_expected.to match_array(expected_models)
+ end
+ end
+end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index b61847b37bb..42c43a59fe2 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -388,6 +388,35 @@ RSpec.describe ProjectPresenter do
end
end
+ describe '#terraform_states_anchor_data' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:anchor_goto_terraform) do
+ have_attributes(
+ is_link: true,
+ label: a_string_including(project.terraform_states.size.to_s),
+ link: presenter.project_terraform_index_path(project)
+ )
+ end
+
+ where(:terraform_states_exists, :can_read_terraform_state, :expected_result) do
+ true | true | ref(:anchor_goto_terraform)
+ true | false | nil
+ false | true | nil
+ false | false | nil
+ end
+
+ with_them do
+ before do
+ allow(project.terraform_states).to receive(:exists?).and_return(terraform_states_exists)
+ allow(presenter).to receive(:can?).with(user, :read_terraform_state,
+ project).and_return(can_read_terraform_state)
+ end
+
+ it { expect(presenter.terraform_states_anchor_data).to match(expected_result) }
+ end
+ end
+
describe '#tags_anchor_data' do
it 'returns tags data' do
expect(presenter.tags_anchor_data).to have_attributes(
diff --git a/spec/presenters/snippet_blob_presenter_spec.rb b/spec/presenters/snippet_blob_presenter_spec.rb
index d7f56c30b5e..cdd02241fbf 100644
--- a/spec/presenters/snippet_blob_presenter_spec.rb
+++ b/spec/presenters/snippet_blob_presenter_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe SnippetBlobPresenter do
let(:file) { 'test.ipynb' }
it 'returns rich notebook content' do
- expect(subject.strip).to eq %Q(<div class="file-content" data-endpoint="#{data_endpoint_url}" data-relative-raw-path="#{data_raw_dir}" id="js-notebook-viewer"></div>)
+ expect(subject.strip).to eq %(<div class="file-content" data-endpoint="#{data_endpoint_url}" data-relative-raw-path="#{data_raw_dir}" id="js-notebook-viewer"></div>)
end
end
@@ -54,7 +54,7 @@ RSpec.describe SnippetBlobPresenter do
let(:file) { 'openapi.yml' }
it 'returns rich openapi content' do
- expect(subject).to eq %Q(<div class="file-content" data-endpoint="#{data_endpoint_url}" id="js-openapi-viewer"></div>\n)
+ expect(subject).to eq %(<div class="file-content" data-endpoint="#{data_endpoint_url}" id="js-openapi-viewer"></div>\n)
end
end
diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb
index 5344a2c2bb7..21cf8ab2c79 100644
--- a/spec/requests/admin/users_controller_spec.rb
+++ b/spec/requests/admin/users_controller_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
+ before do
+ sign_in(admin)
+ end
+
describe 'PUT #block' do
context 'when request format is :json' do
- before do
- sign_in(admin)
- end
-
subject(:request) { put block_admin_user_path(user, format: :json) }
context 'when user was blocked' do
@@ -39,4 +39,16 @@ RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :us
end
end
end
+
+ describe 'PUT #unlock' do
+ before do
+ user.lock_access!
+ end
+
+ subject(:request) { put unlock_admin_user_path(user) }
+
+ it 'unlocks the user' do
+ expect { request }.to change { user.reload.access_locked? }.from(true).to(false)
+ end
+ end
end
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index f2e62533b78..6fad020150c 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -363,7 +363,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :deployment_man
end
it 'returns validation error' do
- expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster'))
+ expect(json_response['message']['platform_kubernetes'].first).to eq(_('Cannot modify managed Kubernetes cluster'))
end
end
diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb
index cad1111b76b..97eb8a2b13f 100644
--- a/spec/requests/api/admin/plan_limits_spec.rb
+++ b/spec/requests/api/admin/plan_limits_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d
expect(json_response['conan_max_file_size']).to eq(Plan.default.actual_limits.conan_max_file_size)
expect(json_response['generic_packages_max_file_size']).to eq(Plan.default.actual_limits.generic_packages_max_file_size)
expect(json_response['helm_max_file_size']).to eq(Plan.default.actual_limits.helm_max_file_size)
+ expect(json_response['limits_history']).to eq(Plan.default.actual_limits.limits_history)
expect(json_response['maven_max_file_size']).to eq(Plan.default.actual_limits.maven_max_file_size)
expect(json_response['npm_max_file_size']).to eq(Plan.default.actual_limits.npm_max_file_size)
expect(json_response['nuget_max_file_size']).to eq(Plan.default.actual_limits.nuget_max_file_size)
@@ -86,7 +87,9 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d
let(:params) { { 'plan_name': 'default' } }
end
- context 'as an admin user' do
+ context 'as an admin user', :freeze_time do
+ let(:current_timestamp) { Time.current.utc.to_i }
+
context 'correct params' do
it 'updates multiple plan limits', :aggregate_failures do
put api(path, admin, admin_mode: true), params: {
@@ -124,6 +127,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d
expect(json_response['enforcement_limit']).to eq(15)
expect(json_response['generic_packages_max_file_size']).to eq(20)
expect(json_response['helm_max_file_size']).to eq(25)
+ expect(json_response['limits_history']).to eq(
+ { "enforcement_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 15 }],
+ "notification_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 90 }],
+ "storage_size_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 80 }] }
+ )
expect(json_response['maven_max_file_size']).to eq(30)
expect(json_response['notification_limit']).to eq(90)
expect(json_response['npm_max_file_size']).to eq(40)
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 7cea744cdb9..6f4e7fd66ed 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -541,7 +541,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
let(:download_headers) do
{ 'Content-Transfer-Encoding' => 'binary',
'Content-Disposition' =>
- %Q(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) }
+ %(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) }
end
it { expect(response).to have_gitlab_http_status(:ok) }
diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb
index d760e4ddf28..d5f60e62b06 100644
--- a/spec/requests/api/ci/pipeline_schedules_spec.rb
+++ b/spec/requests/api/ci/pipeline_schedules_spec.rb
@@ -311,7 +311,8 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra
end
end
- describe 'POST /projects/:id/pipeline_schedules' do
+ # Move this from `shared_context` to `describe` when `ci_refactoring_pipeline_schedule_create_service` is removed.
+ shared_context 'POST /projects/:id/pipeline_schedules' do # rubocop:disable RSpec/ContextWording
let(:params) { attributes_for(:ci_pipeline_schedule) }
context 'authenticated user with valid permissions' do
@@ -368,7 +369,8 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra
end
end
- describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ # Move this from `shared_context` to `describe` when `ci_refactoring_pipeline_schedule_create_service` is removed.
+ shared_context 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
let(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
end
@@ -437,6 +439,18 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra
end
end
+ it_behaves_like 'POST /projects/:id/pipeline_schedules'
+ it_behaves_like 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id'
+
+ context 'when the FF ci_refactoring_pipeline_schedule_create_service is disabled' do
+ before do
+ stub_feature_flags(ci_refactoring_pipeline_schedule_create_service: false)
+ end
+
+ it_behaves_like 'POST /projects/:id/pipeline_schedules'
+ it_behaves_like 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id'
+ end
+
describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
let(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb
index e937c4c2b8f..a1446e1040e 100644
--- a/spec/requests/api/ci/variables_spec.rb
+++ b/spec/requests/api/ci/variables_spec.rb
@@ -48,6 +48,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do
expect(json_response['masked']).to eq(variable.masked?)
expect(json_response['raw']).to eq(variable.raw?)
expect(json_response['variable_type']).to eq('env_var')
+ expect(json_response['description']).to be_nil
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -140,7 +141,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do
it 'creates variable with optional attributes' do
expect do
- post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2', description: 'description' }
end.to change { project.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -150,6 +151,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do
expect(json_response['masked']).to be_falsey
expect(json_response['raw']).to be_falsey
expect(json_response['variable_type']).to eq('file')
+ expect(json_response['description']).to eq('description')
end
it 'does not allow to duplicate variable key' do
@@ -226,7 +228,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do
initial_variable = project.variables.reload.first
value_before = initial_variable.value
- put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true }
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, description: 'updated' }
updated_variable = project.variables.reload.first
@@ -235,6 +237,7 @@ RSpec.describe API::Ci::Variables, feature_category: :secrets_management do
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
expect(updated_variable.variable_type).to eq('file')
+ expect(updated_variable.description).to eq('updated')
end
it 'masks the new value when logging' do
diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb
index 4c1e52df4fc..605fa0d92f6 100644
--- a/spec/requests/api/container_repositories_spec.rb
+++ b/spec/requests/api/container_repositories_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe API::ContainerRepositories, feature_category: :container_registry
let(:created_at) { ::ContainerRepository::MIGRATION_PHASE_1_STARTED_AT + 3.months }
before do
- allow(::Gitlab).to receive(:com?).and_return(on_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(on_com)
repository.update_column(:created_at, created_at)
end
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 9c726e5a5f7..25b99862100 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -6,48 +6,28 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
include WorkhorseHelpers
include_context 'Debian repository shared context', :group, false do
- shared_examples 'a Debian package tracking event' do |action|
- include_context 'Debian repository access', :public, :developer, :basic do
- let(:snowplow_gitlab_standard_context) do
- { project: nil, namespace: container, user: user, property: 'i_package_debian_user' }
- end
-
- it_behaves_like 'a package tracking event', described_class.name, action
- end
- end
-
- shared_examples 'not a Debian package tracking event' do
- include_context 'Debian repository access', :public, :developer, :basic do
- it_behaves_like 'not a package tracking event', described_class.name, /.*/
- end
- end
-
context 'with invalid parameter' do
let(:url) { "/groups/1/-/packages/debian/dists/with+space/InRelease" }
it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
@@ -56,14 +36,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" }
it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
@@ -73,7 +51,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
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 index sha256 endpoint', /^Other SHA256$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do
@@ -82,7 +59,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" }
it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
@@ -92,7 +68,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" }
it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
@@ -101,14 +76,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" }
it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
@@ -118,7 +91,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
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 index sha256 endpoint', /^Other SHA256$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
@@ -139,7 +111,6 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
- it_behaves_like 'a Debian package tracking event', 'pull_package'
context 'for bumping last downloaded at' do
include_context 'Debian repository access', :public, :developer, :basic do
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index b1566860ffc..7f3f633a35c 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -7,22 +7,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
include WorkhorseHelpers
include_context 'Debian repository shared context', :project, false do
- shared_examples 'a Debian package tracking event' do |action|
- include_context 'Debian repository access', :public, :developer, :basic do
- let(:snowplow_gitlab_standard_context) do
- { project: container, namespace: container.namespace, user: user, property: 'i_package_debian_user' }
- end
-
- it_behaves_like 'a package tracking event', described_class.name, action
- end
- end
-
- shared_examples 'not a Debian package tracking event' do
- include_context 'Debian repository access', :public, :developer, :basic do
- it_behaves_like 'not a package tracking event', described_class.name, /.*/
- end
- end
-
shared_examples 'accept GET request on private project with access to package registry for everyone' do
include_context 'Debian repository access', :private, :anonymous, :basic do
before do
@@ -37,14 +21,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" }
it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
- it_behaves_like 'not a Debian package tracking event'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -52,7 +34,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -60,7 +41,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -70,7 +50,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" }
it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -78,7 +57,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
@@ -88,7 +66,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
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 index sha256 endpoint', /^Other SHA256$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -98,7 +75,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" }
it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -109,7 +85,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" }
it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -119,7 +94,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" }
it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -127,7 +101,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
- it_behaves_like 'not a Debian package tracking event'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
@@ -137,7 +110,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
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 index sha256 endpoint', /^Other SHA256$/
- it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -159,7 +131,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
- it_behaves_like 'a Debian package tracking event', 'pull_package'
context 'for bumping last downloaded at' do
include_context 'Debian repository access', :public, :developer, :basic do
@@ -182,13 +153,11 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
it_behaves_like 'Debian packages endpoint catching ObjectStorage::RemoteStoreError'
- it_behaves_like 'a Debian package tracking event', 'push_package'
context 'with codename and component' do
let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
- it_behaves_like 'a Debian package tracking event', 'push_package'
end
context 'with codename and without component' do
@@ -197,8 +166,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
include_context 'Debian repository access', :public, :developer, :basic do
it_behaves_like 'Debian packages GET request', :bad_request, /component is missing/
end
-
- it_behaves_like 'not a Debian package tracking event'
end
end
@@ -209,8 +176,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like "Debian packages upload request", :created, nil
end
- it_behaves_like 'a Debian package tracking event', 'push_package'
-
context 'with codename and component' do
let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
@@ -218,8 +183,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like "Debian packages upload request", :bad_request,
/^file_name Only debs, udebs and ddebs can be directly added to a distribution$/
end
-
- it_behaves_like 'not a Debian package tracking event'
end
end
@@ -227,7 +190,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' }
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
- it_behaves_like 'a Debian package tracking event', 'push_package'
end
end
@@ -237,7 +199,6 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" }
it_behaves_like 'Debian packages write endpoint', 'upload authorize', :created, nil
- it_behaves_like 'not a Debian package tracking event'
end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index d7056adfcb6..82ac2eed83d 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do
)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']['status']).to include(%Q{cannot transition via \"run\"})
+ expect(json_response['message']['status']).to include(%{cannot transition via \"run\"})
end
it 'links merge requests when the deployment status changes to success', :sidekiq_inline do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index c5126dbd1c2..a65dc6e0175 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe API::Discussions, feature_category: :team_planning do
end
context 'when noteable is a WorkItem' do
- let!(:work_item) { create(:work_item, :issue, project: project, author: user) }
+ let!(:work_item) { create(:work_item, project: project, author: user) }
let!(:work_item_note) { create(:discussion_note_on_issue, noteable: work_item, project: project, author: user) }
let(:parent) { project }
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 9a435b3bce9..498e030da0b 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -31,6 +31,14 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(json_response.first).not_to have_key('last_deployment')
end
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: user)
+
+ get api("/projects/#{project.id}/environments"), params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
context 'when filtering' do
let_it_be(:stopped_environment) { create(:environment, :stopped, project: project) }
@@ -132,6 +140,14 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(json_response['external']).to be nil
end
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: user)
+
+ post api("/projects/#{project.id}/environments"), params: { name: "mepmep", job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
it 'requires name to be passed' do
post api("/projects/#{project.id}/environments", user), params: { external_url: 'test.gitlab.com' }
@@ -173,6 +189,15 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: user)
+
+ post api("/projects/#{project.id}/environments/stop_stale"),
+ params: { before: 1.week.ago.to_date.to_s, job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'returns a 400 for bad input date' do
post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.day.ago.to_date.to_s }
@@ -229,6 +254,15 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(json_response['tier']).to eq('production')
end
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: user)
+
+ put api("/projects/#{project.id}/environments/#{environment.id}"),
+ params: { tier: 'production', job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it "won't allow slug to be changed" do
slug = environment.slug
api_url = api("/projects/#{project.id}/environments/#{environment.id}", user)
@@ -261,6 +295,17 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(response).to have_gitlab_http_status(:no_content)
end
+ it 'returns 204 HTTP status when using JOB-TOKEN auth' do
+ environment.stop
+
+ job = create(:ci_build, :running, project: project, user: user)
+
+ delete api("/projects/#{project.id}/environments/#{environment.id}"),
+ params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
it 'returns a 404 for non existing id' do
delete api("/projects/#{project.id}/environments/#{non_existing_record_id}", user)
@@ -291,17 +336,23 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
context 'with a stoppable environment' do
before do
environment.update!(state: :available)
-
- post api("/projects/#{project.id}/environments/#{environment.id}/stop", user)
end
it 'returns a 200' do
+ post api("/projects/#{project.id}/environments/#{environment.id}/stop", user)
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/environment')
+ expect(environment.reload).to be_stopped
end
- it 'actually stops the environment' do
- expect(environment.reload).to be_stopped
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: user)
+
+ post api("/projects/#{project.id}/environments/#{environment.id}/stop"),
+ params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -333,6 +384,15 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(response).to match_response_schema('public_api/v4/environment')
expect(json_response['last_deployment']).to be_present
end
+
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: user)
+
+ get api("/projects/#{project.id}/environments/#{environment.id}"),
+ params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'as non member' do
diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb
index bde90627983..93ad0233ca3 100644
--- a/spec/requests/api/error_tracking/project_settings_spec.rb
+++ b/spec/requests/api/error_tracking/project_settings_spec.rb
@@ -3,10 +3,20 @@
require 'spec_helper'
RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tracking do
- let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:setting) { create(:project_error_tracking_setting, project: project) }
let_it_be(:project_without_setting) { create(:project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
+ let(:user) { maintainer }
+
+ before_all do
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ project_without_setting.add_developer(developer)
+ project_without_setting.add_maintainer(maintainer)
+ end
shared_examples 'returns project settings' do
it 'returns correct project settings' do
@@ -108,10 +118,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'when authenticated as maintainer' do
- before do
- project.add_maintainer(user)
- end
-
context 'with integrated_error_tracking feature enabled' do
it_behaves_like 'returns project settings'
end
@@ -179,10 +185,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
context 'without a project setting' do
let(:project) { project_without_setting }
- before do
- project.add_maintainer(user)
- end
-
it_behaves_like 'returns no project settings'
end
@@ -208,14 +210,14 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'when authenticated as developer' do
- before do
- project.add_developer(user)
- end
+ let(:user) { developer }
it_behaves_like 'returns 403'
end
context 'when authenticated as non-member' do
+ let(:user) { non_member }
+
it_behaves_like 'returns 404'
end
@@ -232,10 +234,6 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'when authenticated as maintainer' do
- before do
- project.add_maintainer(user)
- end
-
it_behaves_like 'returns project settings'
context 'when integrated_error_tracking feature disabled' do
@@ -250,22 +248,18 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
context 'without a project setting' do
let(:project) { project_without_setting }
- before do
- project.add_maintainer(user)
- end
-
it_behaves_like 'returns no project settings'
end
context 'when authenticated as developer' do
- before do
- project.add_developer(user)
- end
+ let(:user) { developer }
it_behaves_like 'returns 403'
end
context 'when authenticated as non-member' do
+ let(:user) { non_member }
+
it_behaves_like 'returns 404'
end
@@ -287,14 +281,8 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
context 'when authenticated' do
context 'as maintainer' do
- before do
- project.add_maintainer(user)
- end
-
context "when integrated" do
context "with existing setting" do
- let(:project) { setting.project }
- let(:setting) { create(:project_error_tracking_setting, :integrated) }
let(:active) { false }
it "updates a setting" do
@@ -302,13 +290,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
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
- )
+ expect(json_response).to include("integrated" => true)
end
end
@@ -366,14 +348,14 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context "as developer" do
- before do
- project.add_developer(user)
- end
+ let(:user) { developer }
it_behaves_like 'returns 403'
end
context 'as non-member' do
+ let(:user) { non_member }
+
it_behaves_like 'returns 404'
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index ed84e3e5f48..ea341703301 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -141,11 +141,9 @@ 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_next_instance_of(Gitlab::Cache::Client) do |instance|
+ expect(instance).to receive(:fetch).with(anything, nil, { cache_identifier: 'API::Files#content_sha', backing_resource: :gitaly }).and_call_original
+ end
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
.to eq(content_sha256)
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
index 2775c3d4c5a..86e2b288890 100644
--- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'get board lists', feature_category: :team_planning do
let(:confidential) { false }
let(:board_parent_type) { board_parent.class.to_s.downcase }
let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] }
- let(:lists_data) { board_data['lists']['nodes'][0] }
+ let(:lists_data) { board_data['lists']['nodes'][1] }
let(:issues_data) { lists_data['issues']['nodes'] }
let(:issue_params) { { filters: { label_name: label2.title, confidential: confidential }, first: 3 } }
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index 2f23e93e2c6..c1ad9bc8728 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe 'get board lists', feature_category: :team_planning do
context 'when using default sorting' do
let!(:label_list) { create(:list, board: board, label: label, position: 10) }
let!(:label_list2) { create(:list, board: board, label: label2, position: 2) }
- let!(:backlog_list) { create(:backlog_list, board: board) }
+ let(:backlog_list) { board.lists.find_by(list_type: :backlog) }
let(:closed_list) { board.lists.find_by(list_type: :closed) }
let(:lists) { [backlog_list, label_list2, label_list, closed_list] }
diff --git a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
index 3b4014c178c..defddc64851 100644
--- a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
@@ -12,9 +12,15 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category:
let(:query) do
%(
- query {
+ query($limit: Int, $sort: CiGroupVariablesSort) {
project(fullPath: "#{project.full_path}") {
- inheritedCiVariables {
+ inheritedCiVariables(first: $limit, sort: $sort) {
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
nodes {
id
key
@@ -32,6 +38,34 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category:
)
end
+ let(:expected_var_b) do
+ {
+ 'id' => subgroup_var.to_global_id.to_s,
+ 'key' => 'SUBGROUP_VAR_B',
+ 'environmentScope' => '*',
+ 'groupName' => subgroup.name,
+ 'groupCiCdSettingsPath' => subgroup_var.group_ci_cd_settings_path,
+ 'masked' => true,
+ 'protected' => false,
+ 'raw' => false,
+ 'variableType' => 'FILE'
+ }
+ end
+
+ let(:expected_var_a) do
+ {
+ 'id' => group_var.to_global_id.to_s,
+ 'key' => 'GROUP_VAR_A',
+ 'environmentScope' => 'production',
+ 'groupName' => group.name,
+ 'groupCiCdSettingsPath' => group_var.group_ci_cd_settings_path,
+ 'masked' => false,
+ 'protected' => true,
+ 'raw' => true,
+ 'variableType' => 'ENV_VAR'
+ }
+ end
+
def create_variables
create(:ci_group_variable, group: group)
create(:ci_group_variable, group: subgroup)
@@ -50,45 +84,115 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category:
end
context 'when user is a project maintainer' do
+ let!(:group_var) do
+ create(:ci_group_variable, group: group, key: 'GROUP_VAR_A',
+ environment_scope: 'production', masked: false, protected: true, raw: true, created_at: 1.day.ago)
+ end
+
+ let!(:subgroup_var) do
+ create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B',
+ masked: true, protected: false, raw: false, variable_type: 'file')
+ end
+
before do
project.add_maintainer(user)
end
it "returns the project's CI variables inherited from its parent group and ancestors" do
- group_var = create(:ci_group_variable, group: group, key: 'GROUP_VAR_A',
- environment_scope: 'production', masked: false, protected: true, raw: true)
-
- subgroup_var = create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B',
- masked: true, protected: false, raw: false, variable_type: 'file')
-
post_graphql(query, current_user: user)
- expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
- {
- 'id' => group_var.to_global_id.to_s,
- 'key' => 'GROUP_VAR_A',
- 'environmentScope' => 'production',
- 'groupName' => group.name,
- 'groupCiCdSettingsPath' => group_var.group_ci_cd_settings_path,
- 'masked' => false,
- 'protected' => true,
- 'raw' => true,
- 'variableType' => 'ENV_VAR'
- },
- {
- 'id' => subgroup_var.to_global_id.to_s,
- 'key' => 'SUBGROUP_VAR_B',
- 'environmentScope' => '*',
- 'groupName' => subgroup.name,
- 'groupCiCdSettingsPath' => subgroup_var.group_ci_cd_settings_path,
- 'masked' => true,
- 'protected' => false,
- 'raw' => false,
- 'variableType' => 'FILE'
- }
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to match_array([
+ expected_var_b, expected_var_a
])
end
+ context 'when limiting the number of results' do
+ it 'returns pagination information' do
+ post_graphql(query, current_user: user, variables: { limit: 1 })
+
+ expect(has_next_page).to be_truthy
+ expect(has_prev_page).to be_falsey
+
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to match_array([
+ expected_var_b
+ ])
+ end
+ end
+
+ describe 'sorting behaviour' do
+ before do
+ post_graphql(query, current_user: user, variables: { sort: sort })
+ end
+
+ shared_examples_for 'unexpected sort parameter' do
+ it 'raises a NoData exception' do
+ expect { graphql_data }.to raise_error(GraphqlHelpers::NoData)
+ end
+ end
+
+ context 'with sort by created_at ascenidng' do
+ let(:sort) { 'CREATED_ASC' }
+
+ it 'returns variables ordered by created_at in ascending order' do
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ expected_var_a, expected_var_b
+ ])
+ end
+ end
+
+ context 'with not existing sort parameter' do
+ let(:sort) { 'WRONG' }
+
+ it_behaves_like 'unexpected sort parameter'
+ end
+
+ context 'with empty sort parameter' do
+ let(:sort) { '' }
+
+ it_behaves_like 'unexpected sort parameter'
+ end
+
+ context 'with no sort parameter' do
+ let(:sort) { nil }
+
+ it 'returns variables by default in descending order by created_at' do
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ expected_var_b, expected_var_a
+ ])
+ end
+ end
+
+ context 'with sort by created_at descending' do
+ let(:sort) { 'CREATED_DESC' }
+
+ it 'returns variables ordered by created_at in descending order' do
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ expected_var_b, expected_var_a
+ ])
+ end
+ end
+
+ context 'with sort by key ascending' do
+ let(:sort) { 'KEY_ASC' }
+
+ it 'returns variables ordered by key in ascending order' do
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ expected_var_a, expected_var_b
+ ])
+ end
+ end
+
+ context 'with sort by key descending' do
+ let(:sort) { 'KEY_DESC' }
+
+ it 'returns variables ordered by key in descending order' do
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ expected_var_b, expected_var_a
+ ])
+ end
+ end
+ end
+
it 'avoids N+1 database queries' do
create_variables
@@ -105,4 +209,16 @@ RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category:
expect(multi).not_to exceed_query_limit(baseline)
end
end
+
+ def pagination_info
+ graphql_data_at('project', 'inheritedCiVariables', 'pageInfo')
+ end
+
+ def has_next_page
+ pagination_info['hasNextPage']
+ end
+
+ def has_prev_page
+ pagination_info['hasPreviousPage']
+ end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 63a657f3962..6acd705c982 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -272,12 +272,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:build1) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline1) }
let_it_be(:build2) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline2) }
- let(:runner_query_fragment) { 'id jobCount' }
let(:query) do
%(
query {
- runner1: runner(id: "#{active_project_runner.to_global_id}") { #{runner_query_fragment} }
- runner2: runner(id: "#{inactive_instance_runner.to_global_id}") { #{runner_query_fragment} }
+ runner1: runner(id: "#{active_project_runner.to_global_id}") { id jobCount(statuses: [RUNNING]) }
+ runner2: runner(id: "#{active_project_runner.to_global_id}") { id jobCount(statuses: FAILED) }
+ runner3: runner(id: "#{active_project_runner.to_global_id}") { id jobCount }
+ runner4: runner(id: "#{inactive_instance_runner.to_global_id}") { id jobCount }
}
)
end
@@ -287,7 +288,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
expect(graphql_data).to match a_hash_including(
'runner1' => a_graphql_entity_for(active_project_runner, job_count: 2),
- 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0)
+ 'runner2' => a_graphql_entity_for(active_project_runner, job_count: 0),
+ 'runner3' => a_graphql_entity_for(active_project_runner, job_count: 2),
+ 'runner4' => a_graphql_entity_for(inactive_instance_runner, job_count: 0)
)
end
@@ -301,7 +304,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
expect(graphql_data).to match a_hash_including(
'runner1' => a_graphql_entity_for(active_project_runner, job_count: 1),
- 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0)
+ 'runner2' => a_graphql_entity_for(active_project_runner, job_count: 0),
+ 'runner3' => a_graphql_entity_for(active_project_runner, job_count: 1),
+ 'runner4' => a_graphql_entity_for(inactive_instance_runner, job_count: 0)
)
end
end
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index 88f63fd59d7..118a11851dd 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -241,7 +241,7 @@ RSpec.describe 'container repository details', feature_category: :container_regi
end
before do
- allow(::Gitlab).to receive(:com?).and_return(on_com)
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(on_com)
container_repository.update_column(:created_at, created_at)
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index c5286b93251..ad21006f99a 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -264,8 +264,8 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
let(:headers) { {} }
before do
- allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args|
- mock_schema.execute(*args)
+ allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args, **kwargs|
+ mock_schema.execute(*args, **kwargs)
end
end
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
deleted file mode 100644
index 143bc1672f8..00000000000
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metrics do
- include GraphqlHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:environment) { create(:environment, project: project) }
- let_it_be(:current_user) { create(:user) }
- let_it_be(:path) { 'config/prometheus/common_metrics.yml' }
- let_it_be(:from) { "2020-04-01T03:29:25Z" }
- let_it_be(:to) { Time.zone.now.advance(minutes: 5) }
- let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: path) }
- let_it_be(:annotation_for_different_env) { create(:metrics_dashboard_annotation, dashboard_path: path) }
- let_it_be(:annotation_for_different_dashboard) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: ".gitlab/dashboards/test.yml") }
- let_it_be(:to_old_annotation) do
- create(:metrics_dashboard_annotation, environment: environment, starting_at: Time.parse(from).advance(minutes: -5), dashboard_path: path)
- end
-
- let_it_be(:to_new_annotation) do
- create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
- end
-
- let(:remove_monitor_metrics) { false }
- let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
- let(:fields) do
- <<~QUERY
- #{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)}
- QUERY
- end
-
- let(:query) do
- %(
- query {
- project(fullPath: "#{project.full_path}") {
- environments(name: "#{environment.name}") {
- nodes {
- metricsDashboard(path: "#{path}") {
- annotations(#{args}) {
- nodes {
- #{fields}
- }
- }
- }
- }
- }
- }
- }
- )
- end
-
- before do
- stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics)
- project.add_developer(current_user)
- post_graphql(query, current_user: current_user)
- end
-
- it_behaves_like 'a working graphql query'
-
- it 'returns annotations' do
- annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
-
- expect(annotations).to match_array [{
- "description" => annotation.description,
- "id" => annotation.to_global_id.to_s,
- "panelId" => annotation.panel_xid,
- "startingAt" => annotation.starting_at.iso8601,
- "endingAt" => nil
- }]
- end
-
- context 'arguments' do
- context 'from is missing' do
- let(:args) { "to: \"#{from}\"" }
-
- it 'returns error' do
- post_graphql(query, current_user: current_user)
-
- expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from")
- end
- end
-
- context 'to is missing' do
- let(:args) { "from: \"#{from}\"" }
-
- it_behaves_like 'a working graphql query'
- end
- end
-
- context 'when metrics dashboard feature is unavailable' do
- let(:remove_monitor_metrics) { true }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- annotations = graphql_data.dig(
- 'project', 'environments', 'nodes', 0, 'metricsDashboard', 'annotations'
- )
-
- expect(annotations).to be_nil
- end
- end
-end
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
deleted file mode 100644
index b7d9b59f5fe..00000000000
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do
- include GraphqlHelpers
-
- let_it_be(:current_user) { create(:user) }
-
- let(:project) { create(:project) }
- let(:environment) { create(:environment, project: project) }
-
- let(:query) do
- graphql_query_for(
- 'project', { 'fullPath' => project.full_path },
- query_graphql_field(
- :environments, { 'name' => environment.name },
- query_graphql_field(
- :nodes, nil,
- query_graphql_field(
- :metricsDashboard, { 'path' => path },
- all_graphql_fields_for('MetricsDashboard'.classify)
- )
- )
- )
- )
- end
-
- context 'for anonymous user' do
- before do
- post_graphql(query, current_user: current_user)
- end
-
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes')
-
- expect(dashboard).to be_nil
- end
- end
- end
-
- context 'for user with developer access' do
- let(:remove_monitor_metrics) { false }
-
- before do
- stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics)
- project.add_developer(current_user)
- post_graphql(query, current_user: current_user)
- end
-
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
- end
-
- context 'invalid dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"])
- end
- end
-
- context 'empty dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"])
- end
- end
-
- context 'metrics dashboard feature is unavailable' do
- let(:remove_monitor_metrics) { true }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to be_nil
- end
- end
- end
-
- context 'requested dashboard can not be found' do
- let(:path) { 'config/prometheus/i_am_not_here.yml' }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to be_nil
- end
- end
- end
-end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
index 3dee7f50af3..ec94760e3f0 100644
--- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
@@ -8,11 +8,13 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
+ let(:api_url) { 'https://prometheus-url.com' }
+
let(:variables) do
{
project_path: project.full_path,
active: false,
- api_url: 'https://prometheus-url.com'
+ api_url: api_url
}
end
@@ -56,7 +58,20 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid
expect(integration_response['apiUrl']).to eq(new_integration.api_url)
end
- [:project_path, :active, :api_url].each do |argument|
+ context 'without api url' do
+ let(:api_url) { nil }
+
+ it 'creates a new integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['apiUrl']).to be_nil
+ end
+ end
+
+ [:project_path, :active].each do |argument|
context "without required argument #{argument}" do
before do
variables.delete(argument)
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index 8791d793cb4..947e7dbcb37 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -53,7 +53,6 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
before do
target_project.add_developer(current_user)
- stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
end
it 'adds the target project to the inbound job token scope' do
@@ -64,20 +63,6 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1)
end
- context 'when FF frozen_outbound_job_token_scopes is disabled' do
- before do
- stub_feature_flags(frozen_outbound_job_token_scopes: false)
- end
-
- it 'adds the target project to the outbound job token scope' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
- end
- end
-
context 'when invalid target project is provided' do
before do
variables[:target_project_path] = 'unknown/project'
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb
index 4a45d255d99..0d5e5f5d2fb 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineSchedulecreate' do
+RSpec.describe 'PipelineSchedulecreate', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
@@ -68,8 +68,9 @@ RSpec.describe 'PipelineSchedulecreate' do
end
end
- context 'when authorized' do
- before do
+ # Move this from `shared_context` to `context` when `ci_refactoring_pipeline_schedule_create_service` is removed.
+ shared_context 'when authorized' do # rubocop:disable RSpec/ContextWording
+ before_all do
project.add_developer(user)
end
@@ -148,4 +149,14 @@ RSpec.describe 'PipelineSchedulecreate' do
end
end
end
+
+ it_behaves_like 'when authorized'
+
+ context 'when the FF ci_refactoring_pipeline_schedule_create_service is disabled' do
+ before do
+ stub_feature_flags(ci_refactoring_pipeline_schedule_create_service: false)
+ end
+
+ it_behaves_like 'when authorized'
+ end
end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb
index b846ff0aec8..e79395bb52c 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/delete_spec.rb
@@ -30,13 +30,13 @@ RSpec.describe 'PipelineScheduleDelete', feature_category: :continuous_integrati
expect(graphql_errors[0]['message'])
.to eq(
"The resource that you are attempting to access does not exist " \
- "or you don't have permission to perform this action"
+ "or you don't have permission to perform this action"
)
end
end
context 'when authorized' do
- before do
+ before_all do
project.add_maintainer(user)
end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb
index 492c6946c99..55ecf8f287e 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/play_spec.rb
@@ -37,13 +37,13 @@ RSpec.describe 'PipelineSchedulePlay', feature_category: :continuous_integration
expect(graphql_errors[0]['message'])
.to eq(
"The resource that you are attempting to access does not exist " \
- "or you don't have permission to perform this action"
+ "or you don't have permission to perform this action"
)
end
end
context 'when authorized', :sidekiq_inline do
- before do
+ before_all do
project.add_maintainer(user)
pipeline_schedule.update_columns(next_run_at: 2.hours.ago)
end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb
index 2d1f1565a73..2d1f1565a73 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/take_ownership_spec.rb
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb
index c1da231a4a6..ec1595f393f 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule/update_spec.rb
@@ -9,6 +9,14 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+ let_it_be(:variable_one) do
+ create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
+ end
+
+ let_it_be(:variable_two) do
+ create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule)
+ end
+
let(:mutation) do
variables = {
id: pipeline_schedule.to_global_id.to_s,
@@ -30,6 +38,7 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
nodes {
key
value
+ variableType
}
}
}
@@ -55,7 +64,7 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
end
context 'when authorized' do
- before do
+ before_all do
project.add_developer(user)
end
@@ -88,8 +97,37 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref])
- expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA')
- expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123')
+ expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['key']).to eq('AAA')
+ expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['value']).to eq('AAA123')
+ end
+ end
+
+ context 'when updating and removing variables' do
+ let(:pipeline_schedule_parameters) do
+ {
+ variables: [
+ { key: 'ABC', value: "ABC123", variableType: 'ENV_VAR', destroy: false },
+ { id: variable_one.to_global_id.to_s,
+ key: 'foo', value: "foovalue",
+ variableType: 'ENV_VAR',
+ destroy: true },
+ { id: variable_two.to_global_id.to_s, key: 'newbar', value: "newbarvalue", variableType: 'ENV_VAR' }
+ ]
+ }
+ end
+
+ it 'processes variables correctly' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['pipelineSchedule']['variables']['nodes'])
+ .to match_array(
+ [
+ { "key" => 'newbar', "value" => 'newbarvalue', "variableType" => 'ENV_VAR' },
+ { "key" => 'ABC', "value" => "ABC123", "variableType" => 'ENV_VAR' }
+ ]
+ )
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 fd92ed198e7..6e101d07b9f 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integration do
include GraphqlHelpers
- before do
- stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
- end
-
let_it_be(:project) do
create(:project,
keep_latest_artifact: true,
@@ -101,22 +97,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
end
end
- context 'when FF frozen_outbound_job_token_scopes is disabled' do
- before do
- stub_feature_flags(frozen_outbound_job_token_scopes: false)
- end
-
- it 'allows setting job_token_scope_enabled to true' do
- project.update!(ci_outbound_job_token_scope_enabled: true)
- post_graphql_mutation(mutation, current_user: user)
-
- project.reload
-
- expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
- end
- end
-
it 'does not update job_token_scope_enabled if not specified' do
variables.except!(:job_token_scope_enabled)
diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
index 1658c277ed0..b697b9f73b7 100644
--- a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
@@ -95,18 +95,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
end
end
- shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
- end
-
- it 'returns an error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.')
- end
- end
-
shared_examples 'when runner is created successfully' do
it do
expected_args = { user: current_user, params: anything }
@@ -139,18 +127,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user has permissions', :enable_admin_mode do
let(:current_user) { admin }
- context 'when :create_runner_workflow_for_admin feature flag is disabled' do
- before do
- stub_feature_flags(create_runner_workflow_for_admin: false)
- end
-
- it 'returns an error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.')
- end
- end
-
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
end
@@ -164,17 +140,12 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
}
end
- before do
- stub_feature_flags(create_runner_workflow_for_namespace: [group])
- end
-
it_behaves_like 'when user does not have permissions'
context 'when user has permissions' do
context 'when user is group owner' do
let(:current_user) { group_owner }
- it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
@@ -226,7 +197,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is admin in admin mode', :enable_admin_mode do
let(:current_user) { admin }
- it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
end
@@ -249,7 +219,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is group owner' do
let(:current_user) { group_owner }
- it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
@@ -304,7 +273,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
context 'when user is admin in admin mode', :enable_admin_mode do
let(:current_user) { admin }
- it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
it_behaves_like 'when runner is created successfully'
it_behaves_like 'when model is invalid returns error'
end
diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb
index e51c057c182..97ead687a82 100644
--- a/spec/requests/api/graphql/mutations/issues/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb
@@ -51,9 +51,7 @@ RSpec.describe 'Update of an existing issue', feature_category: :team_planning d
expect do
post_graphql_mutation(mutation, current_user: current_user)
issue.reload
- end.to change { issue.work_item_type.base_type }.from('issue').to('incident').and(
- change(issue, :issue_type).from('issue').to('incident')
- )
+ end.to change { issue.work_item_type.base_type }.from('issue').to('incident')
end
end
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 e6feba059c4..37bcdf61d23 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -105,7 +105,7 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
context 'as work item' do
let_it_be(:project) { create(:project) }
- let_it_be(:noteable) { create(:work_item, :issue, project: project) }
+ let_it_be(:noteable) { create(:work_item, project: project) }
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
index f40518a574b..9d63eed276d 100644
--- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Destroying a Note', feature_category: :team_planning do
include GraphqlHelpers
- let(:noteable) { create(:work_item, :issue) }
+ let(:noteable) { create(:work_item) }
let!(:note) { create(:note, noteable: noteable, project: noteable.project) }
let(:global_note_id) { GitlabSchema.id_from_object(note).to_s }
let(:variables) { { id: global_note_id } }
diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
index 7918bc860fe..7102f817d4c 100644
--- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Updating a Note', feature_category: :team_planning do
it_behaves_like 'a Note mutation update only with quick actions'
context 'for work item' do
- let(:noteable) { create(:work_item, :issue) }
+ let(:noteable) { create(:work_item) }
let(:note) { create(:note, noteable: noteable, project: noteable.project, note: original_body) }
it_behaves_like 'a Note mutation updates a note successfully'
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index 09e884d9412..c3f818b6627 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management
let!(:snippet_gid) { project.to_gid.to_s }
it 'returns an error' do
- err_message = %Q["#{snippet_gid}" does not represent an instance of Snippet]
+ err_message = %["#{snippet_gid}" does not represent an instance of Snippet]
post_graphql_mutation(mutation, current_user: current_user)
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 60b5795ee9b..ea9516f256c 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -839,14 +839,14 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when changing work item type' do
let_it_be(:work_item) { create(:work_item, :task, project: project) }
- let(:description) { "/type Issue" }
+ let(:description) { "/type issue" }
let(:input) { { 'descriptionWidget' => { 'description' => description } } }
context 'with multiple commands' do
let_it_be(:work_item) { create(:work_item, :task, project: project) }
- let(:description) { "Updating work item\n/type Issue\n/due tomorrow\n/title Foo" }
+ let(:description) { "Updating work item\n/type issue\n/due tomorrow\n/title Foo" }
it 'updates the work item type and other attributes' do
expect do
diff --git a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
deleted file mode 100644
index 3417f9529bd..00000000000
--- a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :incident_management do
- include GraphqlHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:current_user) { create(:user) }
-
- let(:fields) do
- <<~QUERY
- nodes {
- iid
- metricsDashboardUrl
- }
- QUERY
- end
-
- let(:graphql_query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('alertManagementAlerts', {}, fields)
- )
- end
-
- let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
- let(:first_alert) { alerts.first }
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- project.add_developer(current_user)
- end
-
- context 'with self-managed prometheus payload' do
- include_context 'self-managed prometheus alert attributes'
-
- before do
- create(:alert_management_alert, :prometheus, project: project, payload: payload)
- end
-
- it 'includes the correct metrics dashboard url' do
- post_graphql(graphql_query, current_user: current_user)
-
- expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert)
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it 'returns nil' do
- post_graphql(graphql_query, current_user: current_user)
- expect(first_alert['metricsDashboardUrl']).to be_nil
- end
- end
- end
-
- context 'with gitlab-managed prometheus payload' do
- include_context 'gitlab-managed prometheus alert attributes'
-
- before do
- create(:alert_management_alert, :prometheus, project: project, payload: payload, prometheus_alert: prometheus_alert)
- end
-
- it 'includes the correct metrics dashboard url' do
- post_graphql(graphql_query, current_user: current_user)
-
- expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert)
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it 'returns nil' do
- post_graphql(graphql_query, current_user: current_user)
- expect(first_alert['metricsDashboardUrl']).to be_nil
- end
- end
- end
-end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index 55d223daf27..7f586edd510 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -74,7 +74,6 @@ RSpec.describe 'getting Alert Management Alerts', feature_category: :incident_ma
'details' => { 'custom.alert' => 'payload', 'runbook' => 'runbook' },
'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'metricsDashboardUrl' => nil,
'detailsUrl' => triggered_alert.details_url,
'prometheusAlert' => nil,
'runbook' => 'runbook'
diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
index 7587b227d9f..dd383226e17 100644
--- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
+++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'getting incident timeline events', feature_category: :incident_m
let_it_be(:promoted_from_note) { create(:note, project: project, noteable: incident) }
let_it_be(:issue_url) { project_issue_url(private_project, issue) }
let_it_be(:issue_ref) { "#{private_project.full_path}##{issue.iid}" }
- let_it_be(:issue_link) { %Q(<a href="#{issue_url}">#{issue_url}</a>) }
+ let_it_be(:issue_link) { %(<a href="#{issue_url}">#{issue_url}</a>) }
let_it_be(:timeline_event) do
create(
diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb
index aea6cad9e62..2c45c7e9b79 100644
--- a/spec/requests/api/graphql/project/jobs_spec.rb
+++ b/spec/requests/api/graphql/project/jobs_spec.rb
@@ -11,6 +11,9 @@ RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration d
create(:ci_pipeline, project: project, user: user)
end
+ let!(:job1) { create(:ci_build, pipeline: pipeline, name: 'job 1') }
+ let!(:job2) { create(:ci_build, pipeline: pipeline, name: 'job 2') }
+
let(:query) do
<<~QUERY
{
@@ -18,11 +21,6 @@ RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration d
jobs {
nodes {
name
- previousStageJobsAndNeeds {
- nodes {
- name
- }
- }
}
}
}
@@ -30,27 +28,10 @@ RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration d
QUERY
end
- it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
- build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline)
- test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline)
- create(:ci_build, pipeline: pipeline, name: 'docker 1 2', ci_stage: build_stage)
- create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage)
- create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', ci_stage: test_stage)
- test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', ci_stage: test_stage)
- create(:ci_build_need, build: test_job, name: 'docker 1 2')
-
+ it 'fetches jobs' do
post_graphql(query, current_user: user)
+ expect_graphql_errors_to_be_empty
- control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- post_graphql(query, current_user: user)
- end
-
- create(:ci_build, name: 'test-a', ci_stage: test_stage, pipeline: pipeline)
- test_b_job = create(:ci_build, name: 'test-b', ci_stage: test_stage, pipeline: pipeline)
- create(:ci_build_need, build: test_b_job, name: 'docker 2 2')
-
- expect do
- post_graphql(query, current_user: user)
- end.not_to exceed_all_query_limit(control)
+ expect(graphql_data['project']['jobs']['nodes'].pluck('name')).to contain_exactly('job 1', 'job 2')
end
end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index fb1489372fc..d20ee5bfdff 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -337,16 +337,33 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ
end
end
- context 'N+1 queries on pipeline jobs' do
+ context 'N+1 queries on pipeline jobs.previousStageJobsOrNeeds' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:fields) do
<<~FIELDS
- jobs {
+ stages {
nodes {
- previousStageJobsAndNeeds {
+ groups {
nodes {
- name
+ jobs {
+ nodes {
+ previousStageJobsOrNeeds {
+ nodes {
+ ... on JobNeedUnion {
+ ... on CiBuildNeed {
+ id
+ name
+ }
+ ... on CiJob {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ }
}
}
}
@@ -357,13 +374,15 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline)
test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline)
- create(:ci_build, pipeline: pipeline, name: 'docker 1 2', ci_stage: build_stage)
+
+ docker_1_2 = create(:ci_build, pipeline: pipeline, name: 'docker 1 2', ci_stage: build_stage)
create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage)
create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', ci_stage: test_stage)
- test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', ci_stage: test_stage)
- create(:ci_build_need, build: test_job, name: 'docker 1 2')
+ create(:ci_build, :dependent, needed: docker_1_2, pipeline: pipeline, name: 'rspec 2 2', ci_stage: test_stage)
+ # warm up
post_graphql(query, current_user: current_user)
+ expect_graphql_errors_to_be_empty
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: current_user)
@@ -371,7 +390,7 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ
create(:ci_build, name: 'test-a', ci_stage: test_stage, pipeline: pipeline)
test_b_job = create(:ci_build, name: 'test-b', ci_stage: test_stage, pipeline: pipeline)
- create(:ci_build_need, build: test_b_job, name: 'docker 2 2')
+ create(:ci_build, :dependent, needed: test_b_job, pipeline: pipeline, name: 'docker 2 2', ci_stage: test_stage)
expect do
post_graphql(query, current_user: current_user)
@@ -424,6 +443,7 @@ RSpec.describe 'getting pipeline information nested in a project', feature_categ
# warm up
post_graphql(query, current_user: current_user)
+ expect_graphql_errors_to_be_empty
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index ca319ed1b2e..440eb2f52be 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -503,4 +503,51 @@ RSpec.describe 'getting user information', feature_category: :user_management do
end
end
end
+
+ context 'authored merge requests' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:merge_request1) do
+ create(:merge_request, source_project: project, source_branch: '1', author: current_user)
+ end
+
+ let_it_be(:merge_request2) do
+ create(:merge_request, source_project: project, source_branch: '2', author: current_user)
+ end
+
+ let_it_be(:merge_request_different_user) do
+ create(:merge_request, source_project: project, source_branch: '3', author: create(:user))
+ end
+
+ let_it_be(:merge_request_different_group) do
+ create(:merge_request, source_project: create(:project, :public), author: current_user)
+ end
+
+ let_it_be(:merge_request_subgroup) do
+ create(:merge_request, source_project: create(:project, :public, group: subgroup), author: current_user)
+ end
+
+ let(:query) do
+ %(
+ query {
+ currentUser {
+ authoredMergeRequests(groupId: "#{group.to_global_id}") {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns merge requests for the current user for the specified group' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(:current_user, :authored_merge_requests, :nodes).pluck('id')).to contain_exactly(
+ merge_request1.to_global_id.to_s, merge_request2.to_global_id.to_s, merge_request_subgroup.to_global_id.to_s)
+ end
+ end
end
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index 58d0e6a1eb5..7c194627f82 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -453,7 +453,7 @@ RSpec.describe API::GroupClusters, feature_category: :deployment_management do
end
it 'returns validation error' do
- expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster'))
+ expect(json_response['message']['platform_kubernetes'].first).to eq(_('Cannot modify managed Kubernetes cluster'))
end
end
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index 9dd5fe6f7c4..b4add2494b0 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -168,8 +168,9 @@ RSpec.describe API::GroupExport, feature_category: :importers do
end
describe 'relations export' do
+ let(:relation) { 'labels' }
let(:path) { "/groups/#{group.id}/export_relations" }
- let(:download_path) { "/groups/#{group.id}/export_relations/download?relation=labels" }
+ let(:download_path) { "/groups/#{group.id}/export_relations/download?relation=#{relation}" }
let(:status_path) { "/groups/#{group.id}/export_relations/status" }
before do
@@ -196,46 +197,131 @@ RSpec.describe API::GroupExport, feature_category: :importers do
expect(response).to have_gitlab_http_status(:error)
end
end
+
+ context 'when request is to export in batches' do
+ it 'accepts the request' do
+ expect(BulkImports::ExportService)
+ .to receive(:new)
+ .with(portable: group, user: user, batched: true)
+ .and_call_original
+
+ post api(path, user), params: { batched: true }
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
end
describe 'GET /groups/:id/export_relations/download' do
- let(:export) { create(:bulk_import_export, group: group, relation: 'labels') }
- let(:upload) { create(:bulk_import_export_upload, export: export) }
+ context 'when export request is not batched' do
+ let(:export) { create(:bulk_import_export, group: group, relation: 'labels') }
+ let(:upload) { create(:bulk_import_export_upload, export: export) }
+
+ context 'when export file exists' do
+ it 'downloads exported group archive' do
+ upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when export_file.file does not exist' do
+ it 'returns 404' do
+ allow(export).to receive(:upload).and_return(nil)
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('Export file not found')
+ end
+ end
+
+ context 'when export is batched' do
+ let(:relation) { 'milestones' }
+
+ let_it_be(:export) { create(:bulk_import_export, :batched, group: group, relation: 'milestones') }
+
+ it 'returns 400' do
+ export.update!(batched: true)
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Export is batched')
+ end
+ end
+ end
- context 'when export file exists' do
- it 'downloads exported group archive' do
+ context 'when export request is batched' do
+ let(:export) { create(:bulk_import_export, :batched, group: group, relation: 'labels') }
+ let(:upload) { create(:bulk_import_export_upload) }
+ let!(:batch) { create(:bulk_import_export_batch, export: export, upload: upload) }
+
+ it 'downloads exported batch' do
upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
- get api(download_path, user)
+ get api(download_path, user), params: { batched: true, batch_number: batch.batch_number }
expect(response).to have_gitlab_http_status(:ok)
+ expect(response.header['Content-Disposition'])
+ .to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz")
end
- end
- context 'when export_file.file does not exist' do
- it 'returns 404' do
- allow(export).to receive(:upload).and_return(nil)
+ context 'when request is to download not batched export' do
+ it 'returns 400' do
+ get api(download_path, user)
- get api(download_path, user)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Export is batched')
+ end
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Not found')
+ context 'when batch cannot be found' do
+ it 'returns 404' do
+ get api(download_path, user), params: { batched: true, batch_number: 0 }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('Batch not found')
+ end
+ end
+
+ context 'when batch file cannot be found' do
+ it 'returns 404' do
+ batch.upload.destroy!
+
+ get api(download_path, user), params: { batched: true, batch_number: batch.batch_number }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('Batch file not found')
+ end
end
end
end
describe 'GET /groups/:id/export_relations/status' do
- it 'returns a list of relation export statuses' do
- create(:bulk_import_export, :started, group: group, relation: 'labels')
- create(:bulk_import_export, :finished, group: group, relation: 'milestones')
- create(:bulk_import_export, :failed, group: group, relation: 'badges')
+ let_it_be(:started_export) { create(:bulk_import_export, :started, group: group, relation: 'labels') }
+ let_it_be(:finished_export) { create(:bulk_import_export, :finished, group: group, relation: 'milestones') }
+ let_it_be(:failed_export) { create(:bulk_import_export, :failed, group: group, relation: 'badges') }
+ it 'returns a list of relation export statuses' do
get api(status_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'badges')
expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1)
end
+
+ context 'when relation is specified' do
+ it 'return a single relation export status' do
+ get api(status_path, user), params: { relation: 'labels' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['relation']).to eq('labels')
+ expect(json_response['status']).to eq(0)
+ end
+ end
end
context 'when bulk import is disabled' do
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 6849b087211..d0edc181b65 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -56,6 +56,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do
expect(json_response['protected']).to eq(variable.protected?)
expect(json_response['variable_type']).to eq(variable.variable_type)
expect(json_response['environment_scope']).to eq(variable.environment_scope)
+ expect(json_response['description']).to be_nil
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -115,7 +116,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do
it 'creates variable with optional attributes' do
expect do
- post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2', description: 'description' }
end.to change { group.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -126,6 +127,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do
expect(json_response['raw']).to be_falsey
expect(json_response['variable_type']).to eq('file')
expect(json_response['environment_scope']).to eq('*')
+ expect(json_response['description']).to eq('description')
end
it 'does not allow to duplicate variable key' do
@@ -182,7 +184,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do
initial_variable = group.variables.reload.first
value_before = initial_variable.value
- put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, masked: true, raw: true }
+ put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, masked: true, raw: true, description: 'updated' }
updated_variable = group.variables.reload.first
@@ -193,6 +195,7 @@ RSpec.describe API::GroupVariables, feature_category: :secrets_management do
expect(json_response['variable_type']).to eq('file')
expect(json_response['masked']).to be_truthy
expect(json_response['raw']).to be_truthy
+ expect(json_response['description']).to eq('updated')
end
it 'masks the new value when logging' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 2adf71f2a18..5296a8b3e93 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -270,29 +270,17 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
it "includes statistics if requested", :aggregate_failures do
- attributes = {
- storage_size: 4093,
- repository_size: 123,
- wiki_size: 456,
- lfs_objects_size: 234,
- build_artifacts_size: 345,
- pipeline_artifacts_size: 456,
- packages_size: 567,
- snippets_size: 1234,
- uploads_size: 678
- }.stringify_keys
- exposed_attributes = attributes.dup
- exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size')
-
- project1.statistics.update!(attributes)
+ stat_keys = %w[storage_size repository_size wiki_size
+ lfs_objects_size job_artifacts_size pipeline_artifacts_size
+ packages_size snippets_size uploads_size]
get api("/groups", admin, admin_mode: true), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response)
- .to satisfy_one { |group| group['statistics'] == exposed_attributes }
+
+ expect(json_response[0]["statistics"].keys).to match_array(stat_keys)
end
end
@@ -856,6 +844,39 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
end
end
+
+ context "expose shared_runners_setting attribute" do
+ let(:group) { create(:group, shared_runners_enabled: true) }
+
+ before do
+ group.add_owner(user1)
+ end
+
+ it "returns the group with shared_runners_setting as 'enabled'", :aggregate_failures do
+ get api("/groups/#{group.id}", user1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['shared_runners_setting']).to eq("enabled")
+ end
+
+ it "returns the group with shared_runners_setting as 'disabled_and_unoverridable'", :aggregate_failures do
+ group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false)
+
+ get api("/groups/#{group.id}", user1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['shared_runners_setting']).to eq("disabled_and_unoverridable")
+ end
+
+ it "returns the group with shared_runners_setting as 'disabled_and_overridable'", :aggregate_failures do
+ group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true)
+
+ get api("/groups/#{group.id}", user1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['shared_runners_setting']).to eq("disabled_and_overridable")
+ end
+ end
end
end
@@ -1070,6 +1091,50 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
end
+ context 'with owned' do
+ let_it_be(:group) { create(:group) }
+
+ let_it_be(:project1) { create(:project, group: group) }
+ let_it_be(:project1_guest) { create(:user) }
+ let_it_be(:project1_owner) { create(:user) }
+ let_it_be(:project1_maintainer) { create(:user) }
+
+ let_it_be(:project2) { create(:project, group: group) }
+
+ before do
+ project1.add_guest(project1_guest)
+ project1.add_owner(project1_owner)
+ project1.add_maintainer(project1_maintainer)
+
+ project2_owner = project1_owner
+ project2.add_owner(project2_owner)
+ end
+
+ context "as a guest" do
+ it 'returns no projects' do
+ get api("/groups/#{group.id}/projects", project1_guest), params: { owned: true }
+ project_ids = json_response.map { |proj| proj['id'] }
+ expect(project_ids).to match_array([])
+ end
+ end
+
+ context "as a maintainer" do
+ it 'returns no projects' do
+ get api("/groups/#{group.id}/projects", project1_maintainer), params: { owned: true }
+ project_ids = json_response.map { |proj| proj['id'] }
+ expect(project_ids).to match_array([])
+ end
+ end
+
+ context "as an owner" do
+ it 'returns projects with owner access level' do
+ get api("/groups/#{group.id}/projects", project1_owner), params: { owned: true }
+ project_ids = json_response.map { |proj| proj['id'] }
+ expect(project_ids).to match_array([project1.id, project2.id])
+ end
+ end
+ end
+
it "returns the group's projects", :aggregate_failures do
get api("/groups/#{group1.id}/projects", user1)
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 0be9df41e8f..7304437bc42 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -32,6 +32,9 @@ RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_acces
before do
allow_any_instance_of(self.class).to receive(:options).and_return({})
+
+ allow(env['rack.session']).to receive(:enabled?).and_return(true)
+ allow(env['rack.session']).to receive(:loaded?).and_return(true)
end
def warden_authenticate_returns(value)
@@ -567,6 +570,9 @@ RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_acces
context 'using warden authentication' do
before do
+ allow(session).to receive(:enabled?).and_return(true)
+ allow(session).to receive(:loaded?).and_return(true)
+
warden_authenticate_returns admin
env[API::Helpers::SUDO_HEADER] = user.username
end
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 9b5ae72526c..e394b92c0a2 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe API::ImportGithub, feature_category: :importers do
let(:token) { "asdasd12345" }
let(:provider) { :github }
- let(:access_params) { { github_access_token: token } }
+ let(:access_params) { { github_access_token: token, additional_access_tokens: additional_access_tokens } }
+ let(:additional_access_tokens) { nil }
let(:provider_username) { user.username }
let(:provider_user) { double('provider', login: provider_username).as_null_object }
let(:provider_repo) do
@@ -51,7 +52,7 @@ RSpec.describe API::ImportGithub, feature_category: :importers do
it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **access_params)
- .and_return(double(execute: project))
+ .and_return(double(execute: project))
post api("/import/github", user), params: {
target_namespace: user.namespace_path,
@@ -120,6 +121,28 @@ RSpec.describe API::ImportGithub, feature_category: :importers do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ context 'when additional access tokens are provided' do
+ let(:additional_access_tokens) { 'token1,token2' }
+
+ it 'returns 201' do
+ expected_access_params = { github_access_token: token, additional_access_tokens: %w[token1 token2] }
+
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
+ .to receive(:new)
+ .with(provider_repo, provider_repo[:name], user.namespace, user, type: provider, **expected_access_params)
+ .and_return(double(execute: project))
+
+ post api("/import/github", user), params: {
+ target_namespace: user.namespace_path,
+ personal_access_token: token,
+ repo_id: non_existing_record_id,
+ additional_access_tokens: 'token1,token2'
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
end
describe "POST /import/github/cancel" do
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 3c76fba4e2c..09170ca952f 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -122,14 +122,28 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
it 'tracks events and unique events', :aggregate_failures do
request_count = 2
- counters = { gitops_sync: 10, k8s_api_proxy_request: 5, flux_git_push_notifications_total: 42 }
- unique_counters = { agent_users_using_ci_tunnel: [10, 999, 777, 10] }
+ counters = {
+ gitops_sync: 10,
+ k8s_api_proxy_request: 5,
+ flux_git_push_notifications_total: 42,
+ k8s_api_proxy_requests_via_ci_access: 43,
+ k8s_api_proxy_requests_via_user_access: 44
+ }
+ unique_counters = {
+ agent_users_using_ci_tunnel: [10, 999, 777, 10],
+ k8s_api_proxy_requests_unique_users_via_ci_access: [10, 999, 777, 10],
+ k8s_api_proxy_requests_unique_agents_via_ci_access: [10, 999, 777, 10],
+ k8s_api_proxy_requests_unique_users_via_user_access: [10, 999, 777, 10],
+ k8s_api_proxy_requests_unique_agents_via_user_access: [10, 999, 777, 10],
+ flux_git_push_notified_unique_projects: [10, 999, 777, 10]
+ }
expected_counters = {
kubernetes_agent_gitops_sync: request_count * counters[:gitops_sync],
kubernetes_agent_k8s_api_proxy_request: request_count * counters[:k8s_api_proxy_request],
- kubernetes_agent_flux_git_push_notifications_total: request_count * counters[:flux_git_push_notifications_total]
+ kubernetes_agent_flux_git_push_notifications_total: request_count * counters[:flux_git_push_notifications_total],
+ kubernetes_agent_k8s_api_proxy_requests_via_ci_access: request_count * counters[:k8s_api_proxy_requests_via_ci_access],
+ kubernetes_agent_k8s_api_proxy_requests_via_user_access: request_count * counters[:k8s_api_proxy_requests_via_user_access]
}
- expected_hll_count = unique_counters[:agent_users_using_ci_tunnel].uniq.count
request_count.times do
send_request(params: { counters: counters, unique_counters: unique_counters })
@@ -137,13 +151,15 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
expect(Gitlab::UsageDataCounters::KubernetesAgentCounter.totals).to eq(expected_counters)
- expect(
- Gitlab::UsageDataCounters::HLLRedisCounter
- .unique_events(
- event_names: 'agent_users_using_ci_tunnel',
- start_date: Date.current, end_date: Date.current + 10
- )
- ).to eq(expected_hll_count)
+ unique_counters.each do |c, xs|
+ expect(
+ Gitlab::UsageDataCounters::HLLRedisCounter
+ .unique_events(
+ event_names: c.to_s,
+ start_date: Date.current, end_date: Date.current + 10
+ )
+ ).to eq(xs.uniq.count)
+ end
end
end
end
@@ -231,7 +247,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
'gitaly_info' => a_hash_including(
'address' => match(/\.socket$/),
'token' => 'secret',
- 'features' => {}
+ 'features' => Feature::Gitaly.server_feature_flags
),
'gitaly_repository' => a_hash_including(
'storage_name' => project.repository_storage,
@@ -274,7 +290,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
'gitaly_info' => a_hash_including(
'address' => match(/\.socket$/),
'token' => 'secret',
- 'features' => {}
+ 'features' => Feature::Gitaly.server_feature_flags
),
'gitaly_repository' => a_hash_including(
'storage_name' => project.repository_storage,
@@ -531,18 +547,6 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'returns 401 when global flag is disabled' do
- stub_feature_flags(kas_user_access: false)
-
- deployment_project.add_member(user, :developer)
- token = new_token
- public_id = stub_user_session(user, token)
- access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
- send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
it 'returns 401 when user id is not found in session' do
deployment_project.add_member(user, :developer)
token = new_token
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 05a9d98a9d0..7fe17760220 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -3,16 +3,6 @@
require 'spec_helper'
RSpec.describe API::Lint, feature_category: :pipeline_composition do
- describe 'POST /ci/lint' do
- it 'responds with a 410' do
- user = create(:user)
-
- post api('/ci/lint', user), params: { content: "test_job:\n script: ls" }
-
- expect(response).to have_gitlab_http_status(:gone)
- end
- end
-
describe 'GET /projects/:id/ci/lint' do
subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, include_jobs: include_jobs } }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 50e70a9dc0f..f4cac0854e7 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2,7 +2,8 @@
require "spec_helper"
-RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :source_code_management do
+RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :source_code_management,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do
include ProjectForksHelper
let_it_be(:base_time) { Time.now }
@@ -60,13 +61,14 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
end
context 'with merge status recheck projection' do
- it 'does not enqueue a merge status recheck' do
+ it 'does not check mergeability', :sidekiq_inline do
expect(check_service_class).not_to receive(:new)
- get(api(endpoint_path), params: { with_merge_status_recheck: true })
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
expect_successful_response_with_paginated_array
expect(mr_entity['merge_status']).to eq('unchecked')
+ expect(merge_request.reload.merge_status).to eq('unchecked')
end
end
end
@@ -112,16 +114,32 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
end
context 'with merge status recheck projection' do
- it 'checks mergeability asynchronously' do
- expect_next_instances_of(check_service_class, (1..2)) do |service|
- expect(service).not_to receive(:execute)
- expect(service).to receive(:async_execute).and_call_original
+ context 'with batched_api_mergeability_checks FF on' do
+ it 'checks mergeability asynchronously in batch', :sidekiq_inline do
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+
+ expect(merge_request.reload.merge_status).to eq('can_be_merged')
end
+ end
- get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
+ context 'with batched_api_mergeability_checks FF off' do
+ before do
+ stub_feature_flags(batched_api_mergeability_checks: false)
+ end
- expect_successful_response_with_paginated_array
- expect(mr_entity['merge_status']).to eq('checking')
+ it 'checks mergeability asynchronously' do
+ expect_next_instances_of(check_service_class, (1..2)) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute).and_call_original
+ end
+
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('checking')
+ end
end
end
@@ -139,13 +157,14 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
context 'with a reporter role' do
context 'with merge status recheck projection' do
- it 'does not enqueue a merge status recheck' do
+ it 'does not check mergeability', :sidekiq_inline do
expect(check_service_class).not_to receive(:new)
get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
expect_successful_response_with_paginated_array
expect(mr_entity['merge_status']).to eq('unchecked')
+ expect(merge_request.reload.merge_status).to eq('unchecked')
end
end
@@ -154,17 +173,33 @@ RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :sourc
stub_feature_flags(restrict_merge_status_recheck: false)
end
- context 'with merge status recheck projection' do
- it 'does enqueue a merge status recheck' do
- expect_next_instances_of(check_service_class, (1..2)) do |service|
- expect(service).not_to receive(:execute)
- expect(service).to receive(:async_execute).and_call_original
- end
-
+ context 'with batched_api_mergeability_checks FF on' do
+ it 'checks mergeability asynchronously in batch', :sidekiq_inline do
get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
expect_successful_response_with_paginated_array
- expect(mr_entity['merge_status']).to eq('checking')
+
+ expect(merge_request.reload.merge_status).to eq('can_be_merged')
+ end
+ end
+
+ context 'with batched_api_mergeability_checks FF off' do
+ before do
+ stub_feature_flags(batched_api_mergeability_checks: false)
+ end
+
+ context 'with merge status recheck projection' do
+ it 'does enqueue a merge status recheck' do
+ expect_next_instances_of(check_service_class, (1..2)) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute).and_call_original
+ end
+
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('checking')
+ end
end
end
end
diff --git a/spec/requests/api/ml_model_packages_spec.rb b/spec/requests/api/ml_model_packages_spec.rb
index 9c19f522e46..3166298b430 100644
--- a/spec/requests/api/ml_model_packages_spec.rb
+++ b/spec/requests/api/ml_model_packages_spec.rb
@@ -75,6 +75,48 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do
:private | :developer | true | :deploy_token | true | :success
:private | :developer | true | :deploy_token | false | :unauthorized
end
+
+ # :visibility, :user_role, :member, :token_type, :valid_token, :expected_status
+ def download_permissions_tables
+ :public | :developer | true | :personal_access_token | true | :success
+ :public | :guest | true | :personal_access_token | true | :success
+ :public | :developer | true | :personal_access_token | false | :unauthorized
+ :public | :guest | true | :personal_access_token | false | :unauthorized
+ :public | :developer | false | :personal_access_token | true | :success
+ :public | :guest | false | :personal_access_token | true | :success
+ :public | :developer | false | :personal_access_token | false | :unauthorized
+ :public | :guest | false | :personal_access_token | false | :unauthorized
+ :public | :anonymous | false | :personal_access_token | true | :success
+ :private | :developer | true | :personal_access_token | true | :success
+ :private | :guest | true | :personal_access_token | true | :forbidden
+ :private | :developer | true | :personal_access_token | false | :unauthorized
+ :private | :guest | true | :personal_access_token | false | :unauthorized
+ :private | :developer | false | :personal_access_token | true | :not_found
+ :private | :guest | false | :personal_access_token | true | :not_found
+ :private | :developer | false | :personal_access_token | false | :unauthorized
+ :private | :guest | false | :personal_access_token | false | :unauthorized
+ :private | :anonymous | false | :personal_access_token | true | :not_found
+ :public | :developer | true | :job_token | true | :success
+ :public | :guest | true | :job_token | true | :success
+ :public | :developer | true | :job_token | false | :unauthorized
+ :public | :guest | true | :job_token | false | :unauthorized
+ :public | :developer | false | :job_token | true | :success
+ :public | :guest | false | :job_token | true | :success
+ :public | :developer | false | :job_token | false | :unauthorized
+ :public | :guest | false | :job_token | false | :unauthorized
+ :private | :developer | true | :job_token | true | :success
+ :private | :guest | true | :job_token | true | :forbidden
+ :private | :developer | true | :job_token | false | :unauthorized
+ :private | :guest | true | :job_token | false | :unauthorized
+ :private | :developer | false | :job_token | true | :not_found
+ :private | :guest | false | :job_token | true | :not_found
+ :private | :developer | false | :job_token | false | :unauthorized
+ :private | :guest | false | :job_token | false | :unauthorized
+ :public | :developer | true | :deploy_token | true | :success
+ :public | :developer | true | :deploy_token | false | :unauthorized
+ :private | :developer | true | :deploy_token | true | :success
+ :private | :developer | true | :deploy_token | false | :unauthorized
+ end
# rubocop:enable Metrics/AbcSize
end
@@ -82,18 +124,23 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do
project.send("add_#{user_role}", user) if member && user_role != :anonymous
end
- subject(:api_response) do
- request
- response
- end
-
- describe 'PUT /api/v4/projects/:id/packages/ml_models/:package_name/:package_version/:file_name/authorize' do
+ describe 'PUT /api/v4/projects/:id/packages/ml_models/:model_name/:model_version/:file_name/authorize' do
include_context 'ml model authorize permissions table'
let(:token) { tokens[:personal_access_token] }
let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } }
let(:headers) { user_headers.merge(workhorse_headers) }
let(:request) { authorize_upload_file(headers) }
+ let(:model_name) { 'my_package' }
+ let(:file_name) { 'myfile.tar.gz' }
+
+ subject(:api_response) do
+ url = "/projects/#{project.id}/packages/ml_models/#{model_name}/0.0.1/#{file_name}/authorize"
+
+ put api(url), headers: headers
+
+ response
+ end
describe 'user access' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :expected_status) do
@@ -115,16 +162,14 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do
end
describe 'application security' do
- where(:param_name, :param_value) do
- :package_name | 'my-package/../'
- :package_name | 'my-package%2f%2e%2e%2f'
- :file_name | '../.ssh%2fauthorized_keys'
- :file_name | '%2e%2e%2f.ssh%2fauthorized_keys'
+ where(:model_name, :file_name) do
+ 'my-package/../' | 'myfile.tar.gz'
+ 'my-package%2f%2e%2e%2f' | 'myfile.tar.gz'
+ 'my_package' | '../.ssh%2fauthorized_keys'
+ 'my_package' | '%2e%2e%2f.ssh%2fauthorized_keys'
end
with_them do
- let(:request) { authorize_upload_file(headers, param_name => param_value) }
-
it 'rejects malicious request' do
is_expected.to have_gitlab_http_status(:bad_request)
end
@@ -132,7 +177,7 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do
end
end
- describe 'PUT /api/v4/projects/:id/packages/ml_models/:package_name/:package_version/:file_name' do
+ describe 'PUT /api/v4/projects/:id/packages/ml_models/:model_name/:model_version/:file_name' do
include_context 'ml model authorize permissions table'
let_it_be(:file_name) { 'model.md5' }
@@ -143,9 +188,21 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do
let(:params) { { file: temp_file(file_name) } }
let(:file_key) { :file }
let(:send_rewritten_field) { true }
+ let(:model_name) { 'my_package' }
+
+ subject(:api_response) do
+ url = "/projects/#{project.id}/packages/ml_models/#{model_name}/0.0.1/#{file_name}"
- let(:request) do
- upload_file(headers)
+ workhorse_finalize(
+ api(url),
+ method: :put,
+ file_key: file_key,
+ params: params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+
+ response
end
describe 'success' do
@@ -179,22 +236,49 @@ RSpec.describe ::API::MlModelPackages, feature_category: :mlops do
end
end
- def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz')
- url = "/projects/#{project.id}/packages/ml_models/#{package_name}/0.0.1/#{file_name}/authorize"
+ describe 'GET /api/v4/projects/:project_id/packages/ml_models/:model_name/:model_version/:file_name' do
+ include_context 'ml model authorize permissions table'
- put api(url), headers: request_headers
- end
+ let_it_be(:package) { create(:ml_model_package, project: project, name: 'model', version: '0.0.1') }
+ let_it_be(:package_file) { create(:package_file, :generic, package: package, file_name: 'model.md5') }
- def upload_file(request_headers, package_name: 'mypackage')
- url = "/projects/#{project.id}/packages/ml_models/#{package_name}/0.0.1/#{file_name}"
-
- workhorse_finalize(
- api(url),
- method: :put,
- file_key: file_key,
- params: params,
- headers: request_headers,
- send_rewritten_field: send_rewritten_field
- )
+ let(:model_name) { package.name }
+ let(:model_version) { package.version }
+ let(:file_name) { package_file.file_name }
+
+ let(:token) { tokens[:personal_access_token] }
+ let(:user_headers) { { 'HTTP_AUTHORIZATION' => token } }
+ let(:headers) { user_headers.merge(workhorse_headers) }
+
+ subject(:api_response) do
+ url = "/projects/#{project.id}/packages/ml_models/#{model_name}/#{model_version}/#{file_name}"
+
+ get api(url), headers: headers
+
+ response
+ end
+
+ describe 'user access' do
+ where(:visibility, :user_role, :member, :token_type, :valid_token, :expected_status) do
+ download_permissions_tables
+ end
+
+ with_them do
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+ let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
+ end
+
+ if params[:expected_status] == :success
+ it_behaves_like 'process ml model package download'
+ else
+ it { is_expected.to have_gitlab_http_status(expected_status) }
+ end
+ end
+
+ it_behaves_like 'Endpoint not found if read_model_registry not available'
+ end
end
end
diff --git a/spec/requests/api/npm_group_packages_spec.rb b/spec/requests/api/npm_group_packages_spec.rb
index d97c7682b4b..431c59cf1b8 100644
--- a/spec/requests/api/npm_group_packages_spec.rb
+++ b/spec/requests/api/npm_group_packages_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do
+RSpec.describe API::NpmGroupPackages, feature_category: :package_registry,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do
using RSpec::Parameterized::TableSyntax
include_context 'npm api setup'
@@ -152,6 +153,14 @@ RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do
it_behaves_like 'returning response status', params[:expected_status]
end
end
+
+ context 'when metadata cache exists' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package.name, project_id: project.id) }
+
+ subject { get(url) }
+
+ it_behaves_like 'generates metadata response "on-the-fly"'
+ end
end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
@@ -164,12 +173,31 @@ RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do
it_behaves_like 'handling create dist tag requests', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
+
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let(:tag_name) { 'test' }
+ let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ let(:env) { { 'api.request.body': package.version } }
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ subject { put(url, env: env, headers: headers) }
+ end
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests', scope: :group do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
+
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let_it_be(:package_tag) { create(:packages_tag, package: package) }
+
+ let(:tag_name) { package_tag.name }
+ let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ subject { delete(url, headers: headers) }
+ end
end
describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/advisories/bulk' do
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index 97de7fa9e52..4f965d86d66 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -45,6 +45,14 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
end
end
end
+
+ context 'when metadata cache exists' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package.name, project_id: project.id) }
+
+ subject { get(url) }
+
+ it_behaves_like 'generates metadata response "on-the-fly"'
+ end
end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
@@ -57,12 +65,31 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
it_behaves_like 'handling create dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
+
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let(:tag_name) { 'test' }
+ let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ let(:env) { { 'api.request.body': package.version } }
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ subject { put(url, env: env, headers: headers) }
+ end
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
+
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let_it_be(:package_tag) { create(:packages_tag, package: package) }
+
+ let(:tag_name) { package_tag.name }
+ let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ subject { delete(url, headers: headers) }
+ end
end
describe 'POST /api/v4/packages/npm/-/npm/v1/security/advisories/bulk' do
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 60d4bddc502..8c0b9572af3 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
+RSpec.describe API::NpmProjectPackages, feature_category: :package_registry,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do
include ExclusiveLeaseHelpers
include_context 'npm api setup'
@@ -26,6 +27,51 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'rejects invalid package names' do
subject { get(url) }
end
+
+ context 'when metadata cache exists', :aggregate_failures do
+ let!(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package.name, project_id: project.id) }
+ let(:metadata) { Gitlab::Json.parse(npm_metadata_cache.file.read.gsub('dist_tags', 'dist-tags')) }
+
+ subject { get(url) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns response from metadata cache' do
+ expect(Packages::Npm::GenerateMetadataService).not_to receive(:new)
+ expect(Packages::Npm::MetadataCache).to receive(:find_by_package_name_and_project_id)
+ .with(package.name, project.id).and_call_original
+
+ subject
+
+ expect(json_response).to eq(metadata)
+ end
+
+ it 'bumps last_downloaded_at of metadata cache' do
+ expect { subject }
+ .to change { npm_metadata_cache.reload.last_downloaded_at }.from(nil).to(instance_of(ActiveSupport::TimeWithZone))
+ end
+
+ it_behaves_like 'does not enqueue a worker to sync a metadata cache'
+
+ context 'when npm_metadata_cache disabled' do
+ before do
+ stub_feature_flags(npm_metadata_cache: false)
+ end
+
+ it_behaves_like 'generates metadata response "on-the-fly"'
+ end
+
+ context 'when metadata cache file does not exist' do
+ before do
+ FileUtils.rm_rf(npm_metadata_cache.file.path)
+ end
+
+ it_behaves_like 'generates metadata response "on-the-fly"'
+ it_behaves_like 'enqueue a worker to sync a metadata cache'
+ end
+ end
end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
@@ -39,12 +85,31 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'handling create dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
+
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let(:tag_name) { 'test' }
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ let(:env) { { 'api.request.body': package.version } }
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ subject { put(url, env: env, headers: headers) }
+ end
end
describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
+
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let_it_be(:package_tag) { create(:packages_tag, package: package) }
+
+ let(:tag_name) { package_tag.name }
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ subject { delete(url, headers: headers) }
+ end
end
describe 'POST /api/v4/projects/:id/packages/npm/-/npm/v1/security/advisories/bulk' do
@@ -176,12 +241,13 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
subject(:upload_package_with_token) { upload_with_token(package_name, params) }
- shared_examples 'handling invalid record with 400 error' do
+ shared_examples 'handling invalid record with 400 error' do |error_message|
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
expect { upload_package_with_token }
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(error_message)
end
end
@@ -191,7 +257,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" }
let(:params) { upload_params(package_name: package_name) }
- it_behaves_like 'handling invalid record with 400 error'
+ it_behaves_like 'handling invalid record with 400 error', "Validation failed: Name is invalid, Name #{Gitlab::Regex.npm_package_name_regex_message}"
it_behaves_like 'not a package tracking event'
end
@@ -213,7 +279,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
with_them do
let(:params) { upload_params(package_name: package_name, package_version: version) }
- it_behaves_like 'handling invalid record with 400 error'
+ it_behaves_like 'handling invalid record with 400 error', "Validation failed: Version #{Gitlab::Regex.semver_regex_message}"
it_behaves_like 'not a package tracking event'
end
end
@@ -222,7 +288,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
let(:package_name) { "@#{group.path}/my_package_name" }
let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_empty_attachment.json') }
- it_behaves_like 'handling invalid record with 400 error'
+ it_behaves_like 'handling invalid record with 400 error', 'Attachment data is empty.'
it_behaves_like 'not a package tracking event'
end
end
@@ -297,6 +363,10 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'handling upload with different authentications'
end
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ end
+
context 'with an existing package' do
let_it_be(:second_project) { create(:project, namespace: namespace) }
@@ -305,7 +375,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
let(:package_name) { "@#{group.path}/test" }
- it_behaves_like 'handling invalid record with 400 error'
+ it_behaves_like 'handling invalid record with 400 error', 'Validation failed: Package already exists'
it_behaves_like 'not a package tracking event'
context 'with a new version' do
@@ -340,6 +410,11 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['error']).to eq('Package already exists.')
+ end
+
+ it_behaves_like 'does not enqueue a worker to sync a metadata cache' do
+ subject { upload_package_with_token }
end
end
@@ -389,7 +464,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include('Could not obtain package lease.')
+ expect(response.body).to include('Could not obtain package lease. Please try again.')
+ expect(json_response['error']).to eq('Could not obtain package lease. Please try again.')
end
end
@@ -415,15 +491,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
end
+ it_behaves_like 'handling invalid record with 400 error', 'Validation failed: Package json structure is too large. Maximum size is 20000 characters'
it_behaves_like 'not a package tracking event'
-
- it 'returns an error' do
- expect { upload_package_with_token }
- .not_to change { project.packages.count }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include('Validation failed: Package json structure is too large')
- end
end
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index e0e9c944fe4..86ff739da7e 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -27,6 +27,7 @@ itself: # project
- mirror_overwrites_diverged_branches
- mirror_trigger_builds
- mirror_user_id
+ - mirror_branch_regex
- only_mirror_protected_branches
- pages_https_only
- pending_delete
@@ -42,7 +43,7 @@ itself: # project
- runners_token_encrypted
- storage_version
- topic_list
- - mirror_branch_regex
+ - verification_checksum
remapped_attributes:
avatar: avatar_url
build_allow_git_fetch: build_git_strategy
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index c52948a4cb0..99c190757ca 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -443,7 +443,7 @@ RSpec.describe API::ProjectClusters, feature_category: :deployment_management do
it 'returns validation error' do
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']['platform_kubernetes.base'].first)
+ expect(json_response['message']['platform_kubernetes'].first)
.to eq(_('Cannot modify managed Kubernetes cluster'))
end
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 434936c0ee7..3603a71151e 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -567,54 +567,138 @@ RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cach
expect(response).to have_gitlab_http_status(:error)
end
end
+
+ context 'when request is to export in batches' do
+ it 'accepts the request' do
+ expect(BulkImports::ExportService)
+ .to receive(:new)
+ .with(portable: project, user: user, batched: true)
+ .and_call_original
+
+ post api(path, user), params: { batched: true }
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
end
describe 'GET /projects/:id/export_relations/download' do
- let_it_be(:export) { create(:bulk_import_export, project: project, relation: 'labels') }
- let_it_be(:upload) { create(:bulk_import_export_upload, export: export) }
+ context 'when export request is not batched' do
+ let_it_be(:export) { create(:bulk_import_export, project: project, relation: 'labels') }
+ let_it_be(:upload) { create(:bulk_import_export_upload, export: export) }
+
+ context 'when export file exists' do
+ it 'downloads exported project relation archive' do
+ upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz")
+ end
+ end
+
+ context 'when relation is not portable' do
+ let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first }
+
+ it_behaves_like '400 response' do
+ subject(:request) { get api(download_path, user) }
+ end
+ end
+
+ context 'when export file does not exist' do
+ it 'returns 404' do
+ allow(upload).to receive(:export_file).and_return(nil)
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when export is batched' do
+ let(:relation) { 'issues' }
+
+ let_it_be(:export) { create(:bulk_import_export, :batched, project: project, relation: 'issues') }
- context 'when export file exists' do
- it 'downloads exported project relation archive' do
+ it 'returns 400' do
+ export.update!(batched: true)
+
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Export is batched')
+ end
+ end
+ end
+
+ context 'when export request is batched' do
+ let(:export) { create(:bulk_import_export, :batched, project: project, relation: 'labels') }
+ let(:upload) { create(:bulk_import_export_upload) }
+ let!(:batch) { create(:bulk_import_export_batch, export: export, upload: upload) }
+
+ it 'downloads exported batch' do
upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
- get api(download_path, user)
+ get api(download_path, user), params: { batched: true, batch_number: batch.batch_number }
expect(response).to have_gitlab_http_status(:ok)
expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz")
end
- end
- context 'when relation is not portable' do
- let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first }
+ context 'when request is to download not batched export' do
+ it 'returns 400' do
+ get api(download_path, user)
- it_behaves_like '400 response' do
- subject(:request) { get api(download_path, user) }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('Export is batched')
+ end
end
- end
- context 'when export file does not exist' do
- it 'returns 404' do
- allow(upload).to receive(:export_file).and_return(nil)
+ context 'when batch cannot be found' do
+ it 'returns 404' do
+ get api(download_path, user), params: { batched: true, batch_number: 0 }
- get api(download_path, user)
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('Batch not found')
+ end
+ end
+
+ context 'when batch file cannot be found' do
+ it 'returns 404' do
+ batch.upload.destroy!
+
+ get api(download_path, user), params: { batched: true, batch_number: batch.batch_number }
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('Batch file not found')
+ end
end
end
end
describe 'GET /projects/:id/export_relations/status' do
- it 'returns a list of relation export statuses' do
- create(:bulk_import_export, :started, project: project, relation: 'labels')
- create(:bulk_import_export, :finished, project: project, relation: 'milestones')
- create(:bulk_import_export, :failed, project: project, relation: 'project_badges')
+ let_it_be(:started_export) { create(:bulk_import_export, :started, project: project, relation: 'labels') }
+ let_it_be(:finished_export) { create(:bulk_import_export, :finished, project: project, relation: 'milestones') }
+ let_it_be(:failed_export) { create(:bulk_import_export, :failed, project: project, relation: 'project_badges') }
+ it 'returns a list of relation export statuses' do
get api(status_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'project_badges')
expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1)
end
+
+ context 'when relation is specified' do
+ it 'return a single relation export status' do
+ get api(status_path, user), params: { relation: 'labels' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['relation']).to eq('labels')
+ expect(json_response['status']).to eq(0)
+ end
+ end
end
context 'with bulk_import is disabled' do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index c6bf77e5dcf..9d94b5437b7 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -57,6 +57,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks', feature_category: :webhooks do
job_events
deployment_events
releases_events
+ emoji_events
]
end
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index b84b7e9c52d..09991be998a 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -660,6 +660,12 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:no_content)
end
+ it_behaves_like 'enqueue a worker to sync a metadata cache' do
+ let(:package_name) { package1.name }
+
+ subject { delete api(package_url, user) }
+ end
+
context 'with JOB-TOKEN auth' do
let(:job) { create(:ci_build, :running, user: user, project: project) }
@@ -692,6 +698,14 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
delete api(package_url, user)
end
+
+ it_behaves_like 'does not enqueue a worker to sync a metadata cache' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ subject { delete api(package_url, user) }
+ end
end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index bb96771b3d5..f5d1bbbc7e8 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -223,14 +223,6 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
include_examples 'includes container_registry_access_level'
- context 'when projects_preloader_fix is disabled' do
- before do
- stub_feature_flags(projects_preloader_fix: false)
- end
-
- include_examples 'includes container_registry_access_level'
- end
-
it 'includes various project feature fields' do
get api(path, user)
project_response = json_response.find { |p| p['id'] == project.id }
@@ -1843,6 +1835,72 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
end
end
+ describe 'GET /users/:user_id/contributed_projects/' do
+ let(:path) { "/users/#{user3.id}/contributed_projects/" }
+
+ let_it_be(:project1) { create(:project, :public, path: 'my-project') }
+ let_it_be(:project2) { create(:project, :public) }
+ let_it_be(:project3) { create(:project, :public) }
+ let_it_be(:private_project) { create(:project, :private) }
+
+ before do
+ private_project.add_maintainer(user3)
+
+ create(:push_event, project: project1, author: user3)
+ create(:push_event, project: project2, author: user3)
+ create(:push_event, project: private_project, author: user3)
+ end
+
+ it 'returns error when user not found' do
+ get api("/users/#{non_existing_record_id}/contributed_projects/", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ context 'with a public profile' do
+ it 'returns projects filtered by user' do
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] })
+ .to contain_exactly(project1.id, project2.id)
+ end
+ end
+
+ context 'with a private profile' do
+ before do
+ user3.update!(private_profile: true)
+ user3.reload
+ end
+
+ context 'user does not have access to view the private profile' do
+ it 'returns no projects' do
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'user has access to view the private profile as an admin' do
+ it 'returns projects filtered by user' do
+ get api(path, admin, admin_mode: true)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] })
+ .to contain_exactly(project1.id, project2.id, private_project.id)
+ end
+ end
+ end
+ end
+
describe 'POST /projects/user/:id' do
let(:path) { "/projects/user/#{user.id}" }
@@ -2588,12 +2646,12 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
expect(diff).to be_empty, failure_message(diff)
end
- def failure_message(_diff)
+ def failure_message(diff)
<<~MSG
It looks like project's set of exposed attributes is different from the expected set.
The following attributes are missing or newly added:
- {diff.to_a.to_sentence}
+ #{diff.to_a.to_sentence}
Please update #{project_attributes_file} file"
MSG
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 04d5f7ac20a..b79cff5a905 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -121,7 +121,18 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
get api(route, user)
expect(json_response['push_access_levels']).to include(
- a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id)
+ a_hash_including('access_level_description' => deploy_key.title, 'deploy_key_id' => deploy_key.id)
+ )
+ end
+ end
+
+ context 'when a deploy key is not present' do
+ it 'returns null deploy key field' do
+ create(:protected_branch_push_access_level, protected_branch: protected_branch)
+ get api(route, user)
+
+ expect(json_response['push_access_levels']).to include(
+ a_hash_including('deploy_key_id' => nil)
)
end
end
diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb
index c6398e624f8..8a98c751877 100644
--- a/spec/requests/api/protected_tags_spec.rb
+++ b/spec/requests/api/protected_tags_spec.rb
@@ -95,7 +95,18 @@ RSpec.describe API::ProtectedTags, feature_category: :source_code_management do
get api(route, user)
expect(json_response['create_access_levels']).to include(
- a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id)
+ a_hash_including('access_level_description' => deploy_key.title, 'deploy_key_id' => deploy_key.id)
+ )
+ end
+ end
+
+ context 'when a deploy key is not present' do
+ it 'returns null deploy key field' do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag)
+ get api(route, user)
+
+ expect(json_response['create_access_levels']).to include(
+ a_hash_including('deploy_key_id' => nil)
)
end
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 1b331e9c099..0feff90d088 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -688,6 +688,16 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
end
end
+ context 'when user does not have permissions for scope' do
+ it 'returns an empty array' do
+ project.project_feature.update!(issues_access_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
+
+ expect(json_response).to be_empty
+ end
+ end
+
context 'when project does not exist' do
it 'returns 404 error' do
get api('/projects/0/search', user), params: { scope: 'issues', search: 'awesome' }
@@ -861,7 +871,7 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' }
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
+ it_behaves_like 'response is correct', schema: 'public_api/v4/wiki_blobs'
it_behaves_like 'ping counters', scope: :wiki_blobs
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 79e96d7ea3e..dfaba969153 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -80,6 +80,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['valid_runner_registrars']).to match_array(%w(project group))
expect(json_response['ci_max_includes']).to eq(150)
expect(json_response['allow_account_deletion']).to eq(true)
+ expect(json_response['gitlab_shell_operation_limit']).to eq(600)
end
end
@@ -190,13 +191,9 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
default_syntax_highlighting_theme: 2,
projects_api_rate_limit_unauthenticated: 100,
silent_mode_enabled: true,
- slack_app_enabled: true,
- slack_app_id: 'SLACK_APP_ID',
- slack_app_secret: 'SLACK_APP_SECRET',
- slack_app_signing_secret: 'SLACK_APP_SIGNING_SECRET',
- slack_app_verification_token: 'SLACK_APP_VERIFICATION_TOKEN',
valid_runner_registrars: ['group'],
- allow_account_deletion: false
+ allow_account_deletion: false,
+ gitlab_shell_operation_limit: 500
}
expect(response).to have_gitlab_http_status(:ok)
@@ -270,16 +267,23 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['default_syntax_highlighting_theme']).to eq(2)
expect(json_response['projects_api_rate_limit_unauthenticated']).to be(100)
expect(json_response['silent_mode_enabled']).to be(true)
- expect(json_response['slack_app_enabled']).to be(true)
- expect(json_response['slack_app_id']).to eq('SLACK_APP_ID')
- expect(json_response['slack_app_secret']).to eq('SLACK_APP_SECRET')
- expect(json_response['slack_app_signing_secret']).to eq('SLACK_APP_SIGNING_SECRET')
- expect(json_response['slack_app_verification_token']).to eq('SLACK_APP_VERIFICATION_TOKEN')
expect(json_response['valid_runner_registrars']).to eq(['group'])
expect(json_response['allow_account_deletion']).to be(false)
+ expect(json_response['gitlab_shell_operation_limit']).to be(500)
end
end
+ it "updates default_branch_protection_defaults from the default_branch_protection param" do
+ expected_update = ::Gitlab::Access::BranchProtection.protected_against_developer_pushes.stringify_keys
+
+ put api("/application/settings", admin),
+ params: { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+ expect(ApplicationSetting.first.default_branch_protection_defaults).to eq(expected_update)
+ end
+
it "supports legacy performance_bar_allowed_group_id" do
put api("/application/settings", admin),
params: { performance_bar_allowed_group_id: group.full_path }
@@ -550,6 +554,85 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
end
end
+ context 'GitLab for Slack app settings' do
+ let(:settings) do
+ {
+ slack_app_enabled: slack_app_enabled,
+ slack_app_id: slack_app_id,
+ slack_app_secret: slack_app_secret,
+ slack_app_signing_secret: slack_app_signing_secret,
+ slack_app_verification_token: slack_app_verification_token
+ }
+ end
+
+ context 'when GitLab for Slack app is enabled' do
+ let(:slack_app_enabled) { true }
+
+ context 'when other params are blank' do
+ let(:slack_app_id) { nil }
+ let(:slack_app_secret) { nil }
+ let(:slack_app_signing_secret) { nil }
+ let(:slack_app_verification_token) { nil }
+
+ it 'does not update the settings' do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+
+ expect(json_response['slack_app_enabled']).to be(nil)
+ expect(json_response['slack_app_id']).to be(nil)
+ expect(json_response['slack_app_secret']).to be(nil)
+ expect(json_response['slack_app_signing_secret']).to be(nil)
+ expect(json_response['slack_app_verification_token']).to be(nil)
+
+ message = json_response['message']
+
+ expect(message['slack_app_id']).to include("can't be blank")
+ expect(message['slack_app_secret']).to include("can't be blank")
+ expect(message['slack_app_signing_secret']).to include("can't be blank")
+ expect(message['slack_app_verification_token']).to include("can't be blank")
+ end
+ end
+
+ context 'when other params are present' do
+ let(:slack_app_id) { 'ID' }
+ let(:slack_app_secret) { 'SECRET' }
+ let(:slack_app_signing_secret) { 'SIGNING_SECRET' }
+ let(:slack_app_verification_token) { 'VERIFICATION_TOKEN' }
+
+ it 'updates the settings' do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['slack_app_enabled']).to be(true)
+ expect(json_response['slack_app_id']).to eq('ID')
+ expect(json_response['slack_app_secret']).to eq('SECRET')
+ expect(json_response['slack_app_signing_secret']).to eq('SIGNING_SECRET')
+ expect(json_response['slack_app_verification_token']).to eq('VERIFICATION_TOKEN')
+ end
+ end
+ end
+
+ context 'when GitLab for Slack app is not enabled' do
+ let(:slack_app_enabled) { false }
+ let(:slack_app_id) { nil }
+ let(:slack_app_secret) { nil }
+ let(:slack_app_signing_secret) { nil }
+ let(:slack_app_verification_token) { nil }
+
+ it 'allows blank attributes' do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['slack_app_enabled']).to be(false)
+ expect(json_response['slack_app_id']).to be(nil)
+ expect(json_response['slack_app_secret']).to be(nil)
+ expect(json_response['slack_app_signing_secret']).to be(nil)
+ expect(json_response['slack_app_verification_token']).to be(nil)
+ end
+ end
+ end
+
context "missing plantuml_url value when plantuml_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true }
diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb
index baac39abf2c..76190d4e272 100644
--- a/spec/requests/api/statistics_spec.rb
+++ b/spec/requests/api/statistics_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe API::Statistics, 'Statistics', :aggregate_failures, feature_categ
create_list(:note, 2, author: admin, project: projects.first, noteable: issues.first)
create_list(:milestone, 3, project: projects.first)
create(:key, user: admin)
- create(:merge_request, source_project: projects.first)
+ create(:merge_request, :skip_diff_creation, source_project: projects.first)
fork_project(projects.first, admin)
# Make sure the reltuples have been updated
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index 935ddbf4764..c8f1e8d6973 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -164,6 +164,61 @@ RSpec.describe API::UsageData, feature_category: :service_ping do
end
end
+ describe 'POST /usage_data/track_event' do
+ let(:endpoint) { '/usage_data/track_event' }
+ let(:known_event) { 'i_compliance_dashboard' }
+ let(:unknown_event) { 'unknown' }
+ let(:namespace_id) { 123 }
+ let(:project_id) { 123 }
+
+ context 'without CSRF token' do
+ it 'returns forbidden' do
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
+
+ post api(endpoint, user), params: { event: known_event, namespace_id: namespace_id, project_id: project_id }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'usage_data_api feature not enabled' do
+ it 'returns not_found' do
+ stub_feature_flags(usage_data_api: false)
+
+ post api(endpoint, user), params: { event: known_event, namespace_id: namespace_id, project_id: project_id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'without authentication' do
+ it 'returns 401 response' do
+ post api(endpoint), params: { event: known_event, namespace_id: namespace_id, project_id: project_id }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with authentication' do
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
+ end
+
+ context 'with correct params' do
+ it 'returns status ok' do
+ expect(Gitlab::InternalEvents).to receive(:track_event).with(known_event, anything)
+ # allow other events to also get triggered
+ allow(Gitlab::InternalEvents).to receive(:track_event)
+
+ post api(endpoint, user), params: { event: known_event, namespace_id: namespace_id, project_id: project_id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
+
describe 'GET /usage_data/metric_definitions' do
let(:endpoint) { '/usage_data/metric_definitions' }
let(:metric_yaml) do
diff --git a/spec/requests/api/user_runners_spec.rb b/spec/requests/api/user_runners_spec.rb
new file mode 100644
index 00000000000..0e40dcade19
--- /dev/null
+++ b/spec/requests/api/user_runners_spec.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::UserRunners, :aggregate_failures, feature_category: :runner_fleet do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
+
+ describe 'POST /user/runners' do
+ subject(:request) { post api(path, current_user, **post_args), params: runner_attrs }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:group_owner) { create(:user).tap { |user| group.add_owner(user) } }
+ let_it_be(:group_maintainer) { create(:user).tap { |user| group.add_maintainer(user) } }
+ let_it_be(:project_developer) { create(:user).tap { |user| project.add_developer(user) } }
+
+ let(:post_args) { { admin_mode: true } }
+ let(:runner_attrs) { { runner_type: 'instance_type' } }
+ let(:path) { '/user/runners' }
+
+ shared_examples 'when runner creation fails due to authorization' do
+ it 'does not create a runner' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ shared_context 'when user does not have sufficient permissions returns forbidden' do
+ context 'when user is admin and admin mode is disabled' do
+ let(:current_user) { admin }
+ let(:post_args) { { admin_mode: false } }
+
+ it_behaves_like 'when runner creation fails due to authorization'
+ end
+
+ context 'when user is not an admin or a member of the namespace' do
+ let(:current_user) { user }
+
+ it_behaves_like 'when runner creation fails due to authorization'
+ end
+ end
+
+ shared_examples 'creates a runner' do
+ it 'creates a runner' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { Ci::Runner.count }.by(1)
+ end
+ end
+
+ shared_examples 'fails to create runner with expected_status_code' do
+ let(:expected_message) { nil }
+ let(:expected_error) { nil }
+
+ it 'does not create runner' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(expected_status_code)
+ expect(json_response['message']).to include(expected_message) if expected_message
+ expect(json_response['error']).to include(expected_error) if expected_error
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ shared_context 'with request authorized with access token' do
+ let(:current_user) { nil }
+ let(:pat) { create(:personal_access_token, user: token_user, scopes: [scope]) }
+ let(:path) { "/user/runners?private_token=#{pat.token}" }
+
+ %i[create_runner api].each do |scope|
+ context "with #{scope} scope" do
+ let(:scope) { scope }
+
+ it_behaves_like 'creates a runner'
+ end
+ end
+
+ context 'with read_api scope' do
+ let(:scope) { :read_api }
+
+ it_behaves_like 'fails to create runner with expected_status_code' do
+ let(:expected_status_code) { :forbidden }
+ let(:expected_error) { 'insufficient_scope' }
+ end
+ end
+ end
+
+ context 'when runner_type is :instance_type' do
+ let(:runner_attrs) { { runner_type: 'instance_type' } }
+
+ context 'when user has sufficient permissions' do
+ let(:current_user) { admin }
+
+ it_behaves_like 'creates a runner'
+ end
+
+ context 'with admin mode enabled', :enable_admin_mode do
+ let(:token_user) { admin }
+
+ it_behaves_like 'with request authorized with access token'
+ end
+
+ it_behaves_like 'when user does not have sufficient permissions returns forbidden'
+
+ context 'when user is not an admin' do
+ let(:current_user) { user }
+
+ it_behaves_like 'when runner creation fails due to authorization'
+ end
+
+ context 'when model validation fails' do
+ let(:runner_attrs) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } }
+ let(:current_user) { admin }
+
+ it_behaves_like 'fails to create runner with expected_status_code' do
+ let(:expected_status_code) { :bad_request }
+ let(:expected_message) { 'Tags list can not be empty' }
+ end
+ end
+ end
+
+ context 'when runner_type is :group_type' do
+ let(:post_args) { {} }
+
+ context 'when group_id is specified' do
+ let(:runner_attrs) { { runner_type: 'group_type', group_id: group.id } }
+
+ context 'when user has sufficient permissions' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'creates a runner'
+ end
+
+ it_behaves_like 'with request authorized with access token' do
+ let(:token_user) { group_owner }
+ end
+
+ it_behaves_like 'when user does not have sufficient permissions returns forbidden'
+
+ context 'when user is a maintainer' do
+ let(:current_user) { group_maintainer }
+
+ it_behaves_like 'when runner creation fails due to authorization'
+ end
+ end
+
+ context 'when group_id is not specified' do
+ let(:runner_attrs) { { runner_type: 'group_type' } }
+ let(:current_user) { group_owner }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to include('group_id is missing')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+ end
+
+ context 'when runner_type is :project_type' do
+ let(:post_args) { {} }
+
+ context 'when project_id is specified' do
+ let(:runner_attrs) { { runner_type: 'project_type', project_id: project.id } }
+
+ context 'when user has sufficient permissions' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'creates a runner'
+ end
+
+ it_behaves_like 'with request authorized with access token' do
+ let(:token_user) { group_owner }
+ end
+
+ it_behaves_like 'when user does not have sufficient permissions returns forbidden'
+
+ context 'when user is a developer' do
+ let(:current_user) { project_developer }
+
+ it_behaves_like 'when runner creation fails due to authorization'
+ end
+ end
+
+ context 'when project_id is not specified' do
+ let(:runner_attrs) { { runner_type: 'project_type' } }
+ let(:current_user) { group_owner }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to include('project_id is missing')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+ end
+
+ context 'with missing runner_type' do
+ let(:runner_attrs) { {} }
+ let(:current_user) { admin }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('runner_type is missing, runner_type does not have a valid value')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ context 'with unknown runner_type' do
+ let(:runner_attrs) { { runner_type: 'unknown' } }
+ let(:current_user) { admin }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('runner_type does not have a valid value')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ it 'returns a 401 error if unauthorized' do
+ post api(path), params: runner_attrs
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 3737c91adbc..2bbcf6b3f38 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -4851,169 +4851,4 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile
let(:attributable) { user }
let(:other_attributable) { admin }
end
-
- describe 'POST /user/runners', feature_category: :runner_fleet do
- subject(:request) { post api(path, current_user, **post_args), params: runner_attrs }
-
- let_it_be(:group_owner) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, namespace: group) }
-
- let(:post_args) { { admin_mode: true } }
- let(:runner_attrs) { { runner_type: 'instance_type' } }
- let(:path) { '/user/runners' }
-
- before do
- group.add_owner(group_owner)
- end
-
- shared_context 'returns forbidden when user does not have sufficient permissions' do
- let(:current_user) { admin }
- let(:post_args) { { admin_mode: false } }
-
- it 'does not create a runner' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end.not_to change { Ci::Runner.count }
- end
- end
-
- shared_examples 'creates a runner' do
- it 'creates a runner' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- end.to change { Ci::Runner.count }.by(1)
- end
- end
-
- shared_examples 'fails to create runner with :bad_request' do
- it 'does not create runner' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include(expected_error)
- end.not_to change { Ci::Runner.count }
- end
- end
-
- context 'when runner_type is :instance_type' do
- let(:runner_attrs) { { runner_type: 'instance_type' } }
-
- context 'when user has sufficient permissions' do
- let(:current_user) { admin }
-
- it_behaves_like 'creates a runner'
- end
-
- it_behaves_like 'returns forbidden when user does not have sufficient permissions'
-
- context 'when model validation fails' do
- let(:runner_attrs) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } }
- let(:current_user) { admin }
-
- it_behaves_like 'fails to create runner with :bad_request' do
- let(:expected_error) { 'Tags list can not be empty' }
- end
- end
- end
-
- context 'when runner_type is :group_type' do
- let(:post_args) { {} }
-
- context 'when group_id is specified' do
- let(:runner_attrs) { { runner_type: 'group_type', group_id: group.id } }
-
- context 'when user has sufficient permissions' do
- let(:current_user) { group_owner }
-
- it_behaves_like 'creates a runner'
- end
-
- it_behaves_like 'returns forbidden when user does not have sufficient permissions'
- end
-
- context 'when group_id is not specified' do
- let(:runner_attrs) { { runner_type: 'group_type' } }
- let(:current_user) { group_owner }
-
- it 'fails to create runner with :bad_request' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to include('group_id is missing')
- end.not_to change { Ci::Runner.count }
- end
- end
- end
-
- context 'when runner_type is :project_type' do
- let(:post_args) { {} }
-
- context 'when project_id is specified' do
- let(:runner_attrs) { { runner_type: 'project_type', project_id: project.id } }
-
- context 'when user has sufficient permissions' do
- let(:current_user) { group_owner }
-
- it_behaves_like 'creates a runner'
- end
-
- it_behaves_like 'returns forbidden when user does not have sufficient permissions'
- end
-
- context 'when project_id is not specified' do
- let(:runner_attrs) { { runner_type: 'project_type' } }
- let(:current_user) { group_owner }
-
- it 'fails to create runner with :bad_request' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to include('project_id is missing')
- end.not_to change { Ci::Runner.count }
- end
- end
- end
-
- context 'with missing runner_type' do
- let(:runner_attrs) { {} }
- let(:current_user) { admin }
-
- it 'fails to create runner with :bad_request' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('runner_type is missing, runner_type does not have a valid value')
- end.not_to change { Ci::Runner.count }
- end
- end
-
- context 'with unknown runner_type' do
- let(:runner_attrs) { { runner_type: 'unknown' } }
- let(:current_user) { admin }
-
- it 'fails to create runner with :bad_request' do
- expect do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('runner_type does not have a valid value')
- end.not_to change { Ci::Runner.count }
- end
- end
-
- it 'returns a 401 error if unauthorized' do
- post api(path), params: runner_attrs
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 5b50e8a1021..d3d1a2a6cd0 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -236,11 +236,6 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
allow(::Users::ActivityService).to receive(:new).and_return(activity_service)
allow(activity_service).to receive(:execute)
- # During project creation, we need to track the project wiki
- # repository. So it is over the query limit threshold, and we
- # have to adjust it.
- allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(101)
-
expect do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb
index b82cf2b0bad..247535bc990 100644
--- a/spec/requests/groups/observability_controller_spec.rb
+++ b/spec/requests/groups/observability_controller_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
end
it_behaves_like 'observability csp policy' do
+ before_all do
+ group.add_developer(user)
+ end
+
let(:tested_path) { path }
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index b07296a0df2..199138eb3a9 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -807,7 +807,7 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem
end
end
- describe 'to one project' do
+ describe 'to one project', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do
describe 'when user is authenticated' do
describe 'when user has push access to the project' do
before do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 82f972e7f94..217241200ff 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -270,13 +270,20 @@ RSpec.describe 'OpenID Connect requests', feature_category: :system_access do
end
context 'OpenID configuration information' do
+ let(:expected_scopes) do
+ %w[
+ admin_mode api read_user read_api read_repository write_repository sudo openid profile email
+ read_observability write_observability create_runner
+ ]
+ end
+
it 'correctly returns the configuration' do
get '/.well-known/openid-configuration'
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email read_observability write_observability]
+ expect(json_response['scopes_supported']).to match_array expected_scopes
end
context 'with a cross-origin request' do
@@ -286,7 +293,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :system_access do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email read_observability write_observability]
+ expect(json_response['scopes_supported']).to match_array expected_scopes
end
it_behaves_like 'cross-origin GET request'
diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb
index a51a5751831..bd54b50de99 100644
--- a/spec/requests/organizations/organizations_controller_spec.rb
+++ b/spec/requests/organizations/organizations_controller_spec.rb
@@ -5,9 +5,7 @@ require 'spec_helper'
RSpec.describe Organizations::OrganizationsController, feature_category: :cell do
let_it_be(:organization) { create(:organization) }
- describe 'GET #directory' do
- subject(:gitlab_request) { get directory_organization_path(organization) }
-
+ RSpec.shared_examples 'basic organization controller action' do
before do
sign_in(user)
end
@@ -42,4 +40,16 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d
end
end
end
+
+ describe 'GET #show' do
+ subject(:gitlab_request) { get organization_path(organization) }
+
+ it_behaves_like 'basic organization controller action'
+ end
+
+ describe 'GET #groups_and_projects' do
+ subject(:gitlab_request) { get groups_and_projects_organization_path(organization) }
+
+ it_behaves_like 'basic organization controller action'
+ end
end
diff --git a/spec/requests/projects/alert_management_controller_spec.rb b/spec/requests/projects/alert_management_controller_spec.rb
new file mode 100644
index 00000000000..698087bf761
--- /dev/null
+++ b/spec/requests/projects/alert_management_controller_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::AlertManagementController, feature_category: :incident_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:id) { 1 }
+
+ before_all do
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ context 'when user is authorized' do
+ let(:user) { developer }
+
+ it 'shows the page' do
+ get project_alert_management_index_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when user is unauthorized' do
+ let(:user) { reporter }
+
+ it 'shows 404' do
+ get project_alert_management_index_path(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET #details' do
+ context 'when user is authorized' do
+ let(:user) { developer }
+
+ it 'shows the page' do
+ get project_alert_management_alert_path(project, id)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'sets alert id from the route' do
+ get project_alert_management_alert_path(project, id)
+
+ expect(assigns(:alert_id)).to eq(id.to_s)
+ end
+ end
+
+ context 'when user is unauthorized' do
+ let(:user) { reporter }
+
+ it 'shows 404' do
+ get project_alert_management_alert_path(project, id)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/incidents_controller_spec.rb b/spec/requests/projects/incidents_controller_spec.rb
index 460821634b0..9a0d6cdf8ce 100644
--- a/spec/controllers/projects/incidents_controller_spec.rb
+++ b/spec/requests/projects/incidents_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::IncidentsController do
+RSpec.describe Projects::IncidentsController, feature_category: :incident_management do
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
@@ -33,7 +33,7 @@ RSpec.describe Projects::IncidentsController do
describe 'GET #index' do
def make_request
- get :index, params: project_params
+ get project_incidents_path(project)
end
let(:user) { developer }
@@ -65,7 +65,7 @@ RSpec.describe Projects::IncidentsController do
describe 'GET #show' do
def make_request
- get :show, params: project_params(id: resource)
+ get incident_project_issues_path(project, resource)
end
let_it_be(:resource) { create(:incident, project: project) }
@@ -113,10 +113,4 @@ RSpec.describe Projects::IncidentsController do
it_behaves_like 'login required'
end
end
-
- private
-
- def project_params(opts = {})
- opts.reverse_merge(namespace_id: project.namespace, project_id: project)
- end
end
diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb
index 583fd5f586e..1ae65939c86 100644
--- a/spec/requests/projects/issues_controller_spec.rb
+++ b/spec/requests/projects/issues_controller_spec.rb
@@ -17,6 +17,11 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
describe 'GET #new' do
include_context 'group project issue'
+ before do
+ group.add_developer(user)
+ login_as(user)
+ end
+
it_behaves_like "observability csp policy", described_class do
let(:tested_path) do
new_project_issue_path(project)
@@ -26,11 +31,13 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
describe 'GET #show' do
before do
+ group.add_developer(user)
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
diff --git a/spec/requests/projects/merge_requests/creations_spec.rb b/spec/requests/projects/merge_requests/creations_spec.rb
index ace6ef0f7b8..e8a073fef5f 100644
--- a/spec/requests/projects/merge_requests/creations_spec.rb
+++ b/spec/requests/projects/merge_requests/creations_spec.rb
@@ -6,8 +6,13 @@ RSpec.describe 'merge requests creations', feature_category: :code_review_workfl
describe 'GET /:namespace/:project/merge_requests/new' do
include ProjectForksHelper
- let(:project) { create(:project, :repository) }
- let(:user) { project.first_owner }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ group.add_developer(user)
+ end
before do
login_as(user)
@@ -26,16 +31,13 @@ RSpec.describe 'merge requests creations', feature_category: :code_review_workfl
end
it_behaves_like "observability csp policy", Projects::MergeRequests::CreationsController do
- let_it_be(:group) { create(:group) }
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, group: group) }
let(:tested_path) do
project_new_merge_request_path(project, merge_request: {
title: 'Some feature',
- source_branch: 'fix',
- target_branch: 'feature',
- target_project: project,
- source_project: project
+ source_branch: 'fix',
+ target_branch: 'feature',
+ target_project: project,
+ source_project: project
})
end
end
diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb
index 955e6822211..955b6e53686 100644
--- a/spec/requests/projects/merge_requests_controller_spec.rb
+++ b/spec/requests/projects/merge_requests_controller_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code
context 'when logged in' do
before do
+ group.add_developer(user)
login_as(user)
end
diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb
index eec7af99063..4c7491970e1 100644
--- a/spec/requests/projects/ml/candidates_controller_spec.rb
+++ b/spec/requests/projects/ml/candidates_controller_spec.rb
@@ -10,13 +10,17 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
let(:ff_value) { true }
let(:candidate_iid) { candidate.iid }
- let(:model_experiments_enabled) { true }
+ let(:read_model_experiments) { true }
+ let(:write_model_experiments) { true }
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_model_experiments, project)
- .and_return(model_experiments_enabled)
+ .and_return(read_model_experiments)
+ allow(Ability).to receive(:allowed?)
+ .with(user, :write_model_experiments, project)
+ .and_return(write_model_experiments)
sign_in(user)
end
@@ -34,9 +38,9 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
end
end
- shared_examples '404 when model experiments is unavailable' do
+ shared_examples 'requires read_model_experiments' do
context 'when user does not have access' do
- let(:model_experiments_enabled) { false }
+ let(:read_model_experiments) { false }
it_behaves_like 'renders 404'
end
@@ -61,7 +65,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
end
it_behaves_like '404 if candidate does not exist'
- it_behaves_like '404 when model experiments is unavailable'
+ it_behaves_like 'requires read_model_experiments'
end
describe 'DELETE #destroy' do
@@ -83,7 +87,14 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
end
it_behaves_like '404 if candidate does not exist'
- it_behaves_like '404 when model experiments is unavailable'
+
+ describe 'requires write_model_experiments' do
+ let(:write_model_experiments) { false }
+
+ it 'is 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
private
diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb
index e2d26e84f75..9440c716640 100644
--- a/spec/requests/projects/ml/experiments_controller_spec.rb
+++ b/spec/requests/projects/ml/experiments_controller_spec.rb
@@ -15,13 +15,17 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
let(:ff_value) { true }
let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } }
let(:experiment_iid) { experiment.iid }
- let(:model_experiments_enabled) { true }
+ let(:read_model_experiments) { true }
+ let(:write_model_experiments) { true }
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_model_experiments, project)
- .and_return(model_experiments_enabled)
+ .and_return(read_model_experiments)
+ allow(Ability).to receive(:allowed?)
+ .with(user, :write_model_experiments, project)
+ .and_return(write_model_experiments)
sign_in(user)
end
@@ -40,9 +44,9 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
end
- shared_examples '404 when model experiments is unavailable' do
+ shared_examples 'requires read_model_experiments' do
context 'when user does not have access' do
- let(:model_experiments_enabled) { false }
+ let(:read_model_experiments) { false }
it_behaves_like 'renders 404'
end
@@ -100,7 +104,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
end
- it_behaves_like '404 when model experiments is unavailable' do
+ it_behaves_like 'requires read_model_experiments' do
before do
list_experiments
end
@@ -211,7 +215,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
it_behaves_like '404 if experiment does not exist'
- it_behaves_like '404 when model experiments is unavailable'
+ it_behaves_like 'requires read_model_experiments'
end
end
@@ -243,7 +247,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
it_behaves_like '404 if experiment does not exist'
- it_behaves_like '404 when model experiments is unavailable'
+ it_behaves_like 'requires read_model_experiments'
end
end
end
@@ -268,7 +272,14 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
it_behaves_like '404 if experiment does not exist'
- it_behaves_like '404 when model experiments is unavailable'
+
+ describe 'requires write_model_experiments' do
+ let(:write_model_experiments) { false }
+
+ it 'is 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
private
diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb
new file mode 100644
index 00000000000..d03748c8dff
--- /dev/null
+++ b/spec/requests/projects/ml/models_controller_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.first_owner }
+ let_it_be(:model1_a) { create(:ml_model_package, project: project) }
+ let_it_be(:model1_b) { create(:ml_model_package, project: project, name: model1_a.name) }
+ let_it_be(:model2) { create(:ml_model_package, project: project) }
+
+ let(:model_registry_enabled) { true }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :read_model_registry, project)
+ .and_return(model_registry_enabled)
+
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ subject(:index_request) do
+ list_models
+ response
+ end
+
+ it 'renders the template' do
+ expect(index_request).to render_template('projects/ml/models/index')
+ end
+
+ it 'fetches the models using the finder' do
+ expect(::Projects::Ml::ModelFinder).to receive(:new).with(project).and_call_original
+
+ index_request
+ end
+
+ it 'prepares model view using the presenter' do
+ expect(::Ml::ModelsIndexPresenter).to receive(:new).and_call_original
+
+ index_request
+ end
+
+ it 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_models }
+
+ create_list(:ml_model_package, 4, project: project)
+
+ expect { list_models }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when user does not have access' do
+ let(:model_registry_enabled) { false }
+
+ it 'renders 404' do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ private
+
+ def list_models
+ get project_ml_models_path(project)
+ end
+end
diff --git a/spec/requests/projects/packages/package_files_controller_spec.rb b/spec/requests/projects/packages/package_files_controller_spec.rb
index e5849be9f13..4f1793b831d 100644
--- a/spec/requests/projects/packages/package_files_controller_spec.rb
+++ b/spec/requests/projects/packages/package_files_controller_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Projects::Packages::PackageFilesController, feature_category: :pa
subject
expect(response.headers['Content-Disposition'])
- .to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ .to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
it_behaves_like 'bumping the package last downloaded at field'
diff --git a/spec/requests/projects/service_desk/custom_email_controller_spec.rb b/spec/requests/projects/service_desk/custom_email_controller_spec.rb
new file mode 100644
index 00000000000..8ce238ab99c
--- /dev/null
+++ b/spec/requests/projects/service_desk/custom_email_controller_spec.rb
@@ -0,0 +1,380 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: :service_desk do
+ let_it_be_with_reload(:project) do
+ create(:project, :private, service_desk_enabled: true)
+ end
+
+ let_it_be(:custom_email_path) { project_service_desk_custom_email_path(project, format: :json) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:illegitimite_user) { create(:user) }
+
+ let(:message) { instance_double(Mail::Message) }
+ let(:error_cannot_create_custom_email) { s_("ServiceDesk|Cannot create custom email") }
+ let(:error_cannot_update_custom_email) { s_("ServiceDesk|Cannot update custom email") }
+ let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') }
+ let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') }
+
+ let(:custom_email_params) do
+ {
+ custom_email: 'user@example.com',
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: 'user@example.com',
+ smtp_password: 'supersecret'
+ }
+ end
+
+ let(:empty_json_response) do
+ {
+ "custom_email" => nil,
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => nil,
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => nil,
+ "error_message" => nil
+ }
+ end
+
+ before_all do
+ project.add_developer(illegitimite_user)
+ project.add_maintainer(user)
+ end
+
+ shared_examples 'a json response with empty values' do
+ it 'returns json response with empty values' do
+ perform_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(empty_json_response)
+ end
+ end
+
+ shared_examples 'a controller that responds with status' do |status|
+ it "responds with #{status} for GET custom email" do
+ get custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it "responds with #{status} for POST custom email" do
+ post custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it "responds with #{status} for PUT custom email" do
+ put custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it "responds with #{status} for DELETE custom email" do
+ delete custom_email_path
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ shared_examples 'a controller with disabled feature flag with status' do |status|
+ context 'when feature flag service_desk_custom_email is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it_behaves_like 'a controller that responds with status', status
+ end
+ end
+
+ shared_examples 'a deletable resource' do
+ describe 'DELETE custom email' do
+ let(:perform_request) { delete custom_email_path }
+
+ it_behaves_like 'a json response with empty values'
+ end
+ end
+
+ context 'with legitimate user signed in' do
+ before do
+ sign_out(illegitimite_user)
+ sign_in(user)
+ end
+
+ # because CustomEmailController check_feature_flag_enabled responds
+ it_behaves_like 'a controller with disabled feature flag with status', :not_found
+
+ describe 'GET custom email' do
+ let(:perform_request) { get custom_email_path }
+
+ it_behaves_like 'a json response with empty values'
+ end
+
+ describe 'POST custom email' do
+ before do
+ # We send verification email directly
+ allow(message).to receive(:deliver)
+ allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message)
+ end
+
+ it 'adds custom email and kicks of verification' do
+ post custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => custom_email_params[:custom_email],
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => custom_email_params[:smtp_address],
+ "error_message" => nil
+ )
+ end
+
+ context 'when custom_email param is not valid' do
+ it 'does not add custom email' do
+ post custom_email_path, params: custom_email_params.merge(custom_email: 'useratexample.com')
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_cannot_create_custom_email)
+ )
+ end
+ end
+
+ context 'when smtp_password param is not valid' do
+ it 'does not add custom email' do
+ post custom_email_path, params: custom_email_params.merge(smtp_password: '2short')
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_cannot_create_custom_email)
+ )
+ end
+ end
+
+ context 'when the verification process fails fast' do
+ before do
+ # Could not establish connection, invalid host etc.
+ allow(message).to receive(:deliver).and_raise(SocketError)
+ end
+
+ it 'adds custom email and kicks of verification and returns verification error state' do
+ post custom_email_path, params: custom_email_params
+
+ # In terms of "custom email object creation", failing fast on the
+ # verification is a legit state that we don't treat as an error.
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => custom_email_params[:custom_email],
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "failed",
+ "custom_email_verification_error" => "smtp_host_issue",
+ "custom_email_smtp_address" => custom_email_params[:smtp_address],
+ "error_message" => nil
+ )
+ end
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'does not update records' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_cannot_update_custom_email)
+ )
+ end
+ end
+
+ describe 'DELETE custom email' do
+ it 'does not touch any records' do
+ delete custom_email_path
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ empty_json_response.merge("error_message" => error_does_not_exist)
+ )
+ end
+ end
+
+ context 'when custom email is set up' do
+ let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+
+ before do
+ project.reset
+ end
+
+ context 'and verification started' do
+ let!(:verification) do
+ create(:service_desk_custom_email_verification, project: project)
+ end
+
+ it_behaves_like 'a deletable resource'
+
+ describe 'GET custom email' do
+ it 'returns custom email in its current state' do
+ get custom_email_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+
+ describe 'POST custom email' do
+ it 'returns custom email in its current state' do
+ post custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ "custom_email" => custom_email_params[:custom_email],
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => custom_email_params[:smtp_address],
+ "error_message" => error_custom_email_exists
+ )
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'marks custom email as enabled' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "started",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => error_cannot_update_custom_email
+ )
+ end
+ end
+ end
+
+ context 'and verification finished' do
+ let!(:verification) do
+ create(:service_desk_custom_email_verification, project: project, state: :finished, token: nil)
+ end
+
+ it_behaves_like 'a deletable resource'
+
+ describe 'GET custom email' do
+ it 'returns custom email in its current state' do
+ get custom_email_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "finished",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'marks custom email as enabled' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => true,
+ "custom_email_verification_state" => "finished",
+ "custom_email_verification_error" => nil,
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+ end
+
+ context 'and verification failed' do
+ let!(:verification) do
+ create(:service_desk_custom_email_verification,
+ project: project,
+ state: :failed,
+ token: nil,
+ error: :smtp_host_issue
+ )
+ end
+
+ it_behaves_like 'a deletable resource'
+
+ describe 'GET custom email' do
+ it 'returns custom email in its current state' do
+ get custom_email_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "failed",
+ "custom_email_verification_error" => "smtp_host_issue",
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => nil
+ )
+ end
+ end
+
+ describe 'PUT custom email' do
+ let(:custom_email_params) { { custom_email_enabled: true } }
+
+ it 'does not mark custom email as enabled' do
+ put custom_email_path, params: custom_email_params
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ "custom_email" => "user@example.com",
+ "custom_email_enabled" => false,
+ "custom_email_verification_state" => "failed",
+ "custom_email_verification_error" => "smtp_host_issue",
+ "custom_email_smtp_address" => "smtp.example.com",
+ "error_message" => error_cannot_update_custom_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user is anonymous' do
+ before do
+ sign_out(user)
+ sign_out(illegitimite_user)
+ end
+
+ # because Projects::ApplicationController :authenticate_user! responds
+ # with redirect to login page
+ it_behaves_like 'a controller that responds with status', :found
+ it_behaves_like 'a controller with disabled feature flag with status', :found
+ end
+
+ context 'with illegitimate user signed in' do
+ before do
+ sign_out(user)
+ sign_in(illegitimite_user)
+ end
+
+ it_behaves_like 'a controller that responds with status', :not_found
+ # because CustomEmailController check_feature_flag_enabled responds
+ it_behaves_like 'a controller with disabled feature flag with status', :not_found
+ end
+end
diff --git a/spec/controllers/projects/service_desk_controller_spec.rb b/spec/requests/projects/service_desk_controller_spec.rb
index 6b914ac8f19..54fe176e244 100644
--- a/spec/controllers/projects/service_desk_controller_spec.rb
+++ b/spec/requests/projects/service_desk_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ServiceDeskController do
+RSpec.describe Projects::ServiceDeskController, feature_category: :service_desk do
let_it_be(:project) do
create(:project, :private, :custom_repo,
service_desk_enabled: true,
@@ -11,17 +11,20 @@ RSpec.describe Projects::ServiceDeskController do
let_it_be(:user) { create(:user) }
+ before_all do
+ project.add_maintainer(user)
+ end
+
before do
- allow(Gitlab::Email::IncomingEmail).to receive(:enabled?) { true }
- allow(Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?) { true }
+ allow(Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
- project.add_maintainer(user)
sign_in(user)
end
- describe 'GET service desk properties' do
+ describe 'GET #show' do
it 'returns service_desk JSON data' do
- get :show, params: { namespace_id: project.namespace.to_param, project_id: project }, format: :json
+ get project_service_desk_path(project, format: :json)
expect(json_response["service_desk_address"]).to match(/\A[^@]+@[^@]+\z/)
expect(json_response["service_desk_enabled"]).to be_truthy
@@ -35,7 +38,7 @@ RSpec.describe Projects::ServiceDeskController do
project.add_guest(guest)
sign_in(guest)
- get :show, params: { namespace_id: project.namespace.to_param, project_id: project }, format: :json
+ get project_service_desk_path(project, format: :json)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -45,7 +48,7 @@ RSpec.describe Projects::ServiceDeskController do
it 'returns template_file_missing as false' do
create(:service_desk_setting, project: project, issue_template_key: 'service_desk')
- get :show, params: { namespace_id: project.namespace.to_param, project_id: project }, format: :json
+ get project_service_desk_path(project, format: :json)
response_hash = Gitlab::Json.parse(response.body)
expect(response_hash['template_file_missing']).to eq(false)
@@ -57,20 +60,18 @@ RSpec.describe Projects::ServiceDeskController do
service = ServiceDeskSetting.new(project_id: project.id, issue_template_key: 'deleted')
service.save!(validate: false)
- get :show, params: { namespace_id: project.namespace.to_param, project_id: project }, format: :json
+ get project_service_desk_path(project, format: :json)
expect(json_response['template_file_missing']).to eq(true)
end
end
end
- describe 'PUT service desk properties' do
+ describe 'PUT #update' do
it 'toggles services desk incoming email' do
project.update!(service_desk_enabled: false)
- put :update, params: { namespace_id: project.namespace.to_param,
- project_id: project,
- service_desk_enabled: true }, format: :json
+ put project_service_desk_refresh_path(project, format: :json), params: { service_desk_enabled: true }
expect(json_response["service_desk_address"]).to be_present
expect(json_response["service_desk_enabled"]).to be_truthy
@@ -78,9 +79,7 @@ RSpec.describe Projects::ServiceDeskController do
end
it 'sets issue_template_key' do
- put :update, params: { namespace_id: project.namespace.to_param,
- project_id: project,
- issue_template_key: 'service_desk' }, format: :json
+ put project_service_desk_refresh_path(project, format: :json), params: { issue_template_key: 'service_desk' }
settings = project.service_desk_setting
expect(settings).to be_present
@@ -90,9 +89,7 @@ RSpec.describe Projects::ServiceDeskController do
end
it 'returns an error when update of service desk settings fails' do
- put :update, params: { namespace_id: project.namespace.to_param,
- project_id: project,
- issue_template_key: 'invalid key' }, format: :json
+ put project_service_desk_refresh_path(project, format: :json), params: { issue_template_key: 'invalid key' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Issue template key is empty or does not exist')
@@ -103,7 +100,7 @@ RSpec.describe Projects::ServiceDeskController do
it 'renders 404' do
sign_in(other_user)
- put :update, params: { namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true }, format: :json
+ put project_service_desk_refresh_path(project, format: :json), params: { service_desk_enabled: true }
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/projects/tracing_controller_spec.rb b/spec/requests/projects/tracing_controller_spec.rb
new file mode 100644
index 00000000000..eecaa0d962a
--- /dev/null
+++ b/spec/requests/projects/tracing_controller_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::TracingController, feature_category: :tracing do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+ let(:path) { nil }
+ let(:observability_tracing_ff) { true }
+
+ subject do
+ get path
+ response
+ end
+
+ describe 'GET #index' do
+ before do
+ stub_feature_flags(observability_tracing: observability_tracing_ff)
+ sign_in(user)
+ end
+
+ let(:path) { project_tracing_index_path(project) }
+
+ it_behaves_like 'observability csp policy' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ let(:tested_path) { path }
+ end
+
+ context 'when user does not have permissions' do
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user has permissions' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'returns 200' do
+ expect(subject).to have_gitlab_http_status(:ok)
+ end
+
+ it 'renders the js-tracing element correctly' do
+ element = Nokogiri::HTML.parse(subject.body).at_css('#js-tracing')
+
+ expected_view_model = {
+ tracingUrl: Gitlab::Observability.tracing_url(project),
+ provisioningUrl: Gitlab::Observability.provisioning_url(project),
+ oauthUrl: Gitlab::Observability.oauth_url
+ }.to_json
+ expect(element.attributes['data-view-model'].value).to eq(expected_view_model)
+ end
+
+ context 'when feature is disabled' do
+ let(:observability_tracing_ff) { false }
+
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb
index f2d4e288ddc..365b20ad4aa 100644
--- a/spec/requests/search_controller_spec.rb
+++ b/spec/requests/search_controller_spec.rb
@@ -39,7 +39,8 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc
context 'for issues scope' do
let(:object) { :issue }
- let(:creation_args) { { project: project, title: 'foo' } }
+ let(:labels) { create_list(:label, 3, project: project) }
+ let(:creation_args) { { project: project, title: 'foo', labels: labels } }
let(:params) { { search: 'foo', scope: 'issues' } }
# some N+1 queries still exist
# each issue runs an extra query for group namespaces
@@ -50,8 +51,9 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc
context 'for merge_requests scope' do
let(:creation_traits) { [:unique_branches] }
+ let(:labels) { create_list(:label, 3, project: project) }
let(:object) { :merge_request }
- let(:creation_args) { { source_project: project, title: 'bar' } }
+ let(:creation_args) { { source_project: project, title: 'bar', labels: labels } }
let(:params) { { search: 'bar', scope: 'merge_requests' } }
# some N+1 queries still exist
# each merge request runs an extra query for project routes
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index c49dbb6a269..f96d7864782 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -598,15 +598,10 @@ RSpec.describe UsersController, feature_category: :user_management do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to be_empty
end
-
- it 'does not list projects aimed for deletion' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:contributed_projects)).to eq([project])
- end
end
%i(html json).each do |format|
- context "format: #{format}" do
+ context "with format: #{format}" do
let(:format) { format }
context 'with public profile' do
@@ -626,6 +621,13 @@ RSpec.describe UsersController, feature_category: :user_management do
let(:user) { create(:admin) }
it_behaves_like 'renders contributed projects'
+
+ if format == :json
+ it 'does not list projects aimed for deletion' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to include aimed_for_deletion_project.name
+ end
+ end
end
end
end
@@ -652,15 +654,10 @@ RSpec.describe UsersController, feature_category: :user_management do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to be_empty
end
-
- it 'does not list projects aimed for deletion' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:starred_projects)).to eq([project])
- end
end
%i(html json).each do |format|
- context "format: #{format}" do
+ context "with format: #{format}" do
let(:format) { format }
context 'with public profile' do
@@ -680,6 +677,13 @@ RSpec.describe UsersController, feature_category: :user_management do
let(:user) { create(:admin) }
it_behaves_like 'renders starred projects'
+
+ if format == :json
+ it 'does not list projects aimed for deletion' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to include aimed_for_deletion_project.name
+ end
+ end
end
end
end
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index 6325ecc1184..f3f8e4a1a83 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_redis_rate_limiting,
-feature_category: :user_management do
+ feature_category: :instance_resiliency do
include SessionHelpers
include EmailHelpers
diff --git a/spec/routing/organizations/organizations_controller_routing_spec.rb b/spec/routing/organizations/organizations_controller_routing_spec.rb
index 5b6124300ba..2b43f6d3afa 100644
--- a/spec/routing/organizations/organizations_controller_routing_spec.rb
+++ b/spec/routing/organizations/organizations_controller_routing_spec.rb
@@ -5,8 +5,13 @@ require 'spec_helper'
RSpec.describe Organizations::OrganizationsController, :routing, feature_category: :cell do
let_it_be(:organization) { build(:organization) }
- it 'routes to #directory' do
- expect(get("/-/organizations/#{organization.path}/directory"))
- .to route_to('organizations/organizations#directory', organization_path: organization.path)
+ it 'routes to #show' do
+ expect(get("/-/organizations/#{organization.path}"))
+ .to route_to('organizations/organizations#show', organization_path: organization.path)
+ end
+
+ it 'routes to #groups_and_projects' do
+ expect(get("/-/organizations/#{organization.path}/groups_and_projects"))
+ .to route_to('organizations/organizations#groups_and_projects', organization_path: organization.path)
end
end
diff --git a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
index e35705ae791..1b41e140454 100644
--- a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
+++ b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
@@ -41,10 +41,10 @@ RSpec.describe RuboCop::Cop::AvoidReturnFromBlocks do
RUBY
end
- shared_examples 'examples with whitelisted method' do |whitelisted_method|
- it "doesn't flag violation for return inside #{whitelisted_method}" do
+ shared_examples 'examples with allowlisted method' do |allowlisted_method|
+ it "doesn't flag violation for return inside #{allowlisted_method}" do
expect_no_offenses(<<~RUBY)
- items.#{whitelisted_method} do |item|
+ items.#{allowlisted_method} do |item|
do_something
return if something_else
end
@@ -52,8 +52,8 @@ RSpec.describe RuboCop::Cop::AvoidReturnFromBlocks do
end
end
- %i[each each_filename times loop].each do |whitelisted_method|
- it_behaves_like 'examples with whitelisted method', whitelisted_method
+ %i[each each_filename times loop].each do |allowlisted_method|
+ it_behaves_like 'examples with allowlisted method', allowlisted_method
end
shared_examples 'examples with def methods' do |def_method|
diff --git a/spec/rubocop/cop/background_migration/avoid_silent_rescue_exceptions_spec.rb b/spec/rubocop/cop/background_migration/avoid_silent_rescue_exceptions_spec.rb
new file mode 100644
index 00000000000..ea4f7d5fca8
--- /dev/null
+++ b/spec/rubocop/cop/background_migration/avoid_silent_rescue_exceptions_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/background_migration/avoid_silent_rescue_exceptions'
+
+RSpec.describe RuboCop::Cop::BackgroundMigration::AvoidSilentRescueExceptions, feature_category: :database do
+ shared_examples 'expecting offense when' do |node|
+ it 'throws offense when rescuing exceptions without re-raising them' do
+ %w[Gitlab::BackgroundMigration::BatchedMigrationJob BatchedMigrationJob].each do |base_class|
+ expect_offense(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < #{base_class}
+ #{node}
+ end
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ shared_examples 'not expecting offense when' do |node|
+ it 'does not throw any offense if exception is re-raised' do
+ %w[Gitlab::BackgroundMigration::BatchedMigrationJob BatchedMigrationJob].each do |base_class|
+ expect_no_offenses(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < #{base_class}
+ #{node}
+ end
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context "when the migration class doesn't inherits from BatchedMigrationJob" do
+ it 'does not throw any offense' do
+ expect_no_offenses(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyClass < ::Gitlab::BackgroundMigration::Logger
+ def my_method
+ execute
+ rescue StandardError => error
+ puts error.message
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when the migration class inherits from BatchedMigrationJob' do
+ context 'when specifying an error class' do
+ it_behaves_like 'expecting offense when', <<~RUBY
+ def perform
+ connection.execute('SELECT 1;')
+ rescue JSON::ParserError
+ ^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ logger.error(message: error.message, class: self.class.name)
+ end
+ RUBY
+
+ it_behaves_like 'expecting offense when', <<~RUBY
+ def perform
+ connection.execute('SELECT 1;')
+ rescue StandardError, ActiveRecord::StatementTimeout => error
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ logger.error(message: error.message, class: self.class.name)
+ end
+ RUBY
+
+ it_behaves_like 'not expecting offense when', <<~RUBY
+ def perform
+ connection.execute('SELECT 1;')
+ rescue StandardError, ActiveRecord::StatementTimeout => error
+ logger.error(message: error.message, class: self.class.name)
+ raise error
+ end
+ RUBY
+ end
+
+ context 'without specifying an error class' do
+ it_behaves_like 'expecting offense when', <<~RUBY
+ def perform
+ connection.execute('SELECT 1;')
+ rescue => error
+ ^^^^^^ #{described_class::MSG}
+ logger.error(message: error.message, class: self.class.name)
+ end
+ RUBY
+
+ it_behaves_like 'not expecting offense when', <<~RUBY
+ def perform
+ connection.execute('SELECT 1;')
+ rescue => error
+ logger.error(message: error.message, class: self.class.name)
+ raise error
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
index 96ff01108c3..4b7ea6b72e5 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -48,35 +48,35 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo")|, 'foo'
+ include_examples 'sets flag as used', %|#{feature_flag_method}("foo")|, 'foo'
end
context 'a symbol feature flag' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(:foo)|, 'foo'
+ include_examples 'sets flag as used', %|#{feature_flag_method}(:foo)|, 'foo'
end
context 'an interpolated string feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'a string with a "/" in it' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}("bar/baz")|, 'bar_baz'
+ include_examples 'sets flag as used', %|#{feature_flag_method}("bar/baz")|, 'bar_baz'
end
context 'an interpolated string feature flag with a string prefix and suffix' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(123)|
end
end
end
@@ -87,31 +87,31 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo")|, 'gitaly_foo'
+ include_examples 'sets flag as used', %|#{feature_flag_method}("foo")|, 'gitaly_foo'
end
context 'a symbol feature flag' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(:foo)|, 'gitaly_foo'
+ include_examples 'sets flag as used', %|#{feature_flag_method}(:foo)|, 'gitaly_foo'
end
context 'an interpolated string feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(123)|
end
end
end
@@ -126,15 +126,15 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
end
context 'an interpolated string feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|experiment("foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|experiment("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|experiment(:"foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|experiment(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
- include_examples 'does not set any flags as used', %Q|experiment(:"foo_\#{bar}_baz")|
+ include_examples 'does not set any flags as used', %|experiment(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
@@ -151,31 +151,31 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, "baz")|, 'baz'
+ include_examples 'sets flag as used', %|#{feature_flag_method}(arg, "baz")|, 'baz'
end
context 'a symbol feature flag' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, :baz)|, 'baz'
+ include_examples 'sets flag as used', %|#{feature_flag_method}(arg, :baz)|, 'baz'
end
context 'an interpolated string feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, "foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|#{feature_flag_method}(arg, "foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
- include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, :"foo_\#{bar}")|, %w[foo_hello foo_world]
+ include_examples 'sets flag as used', %|#{feature_flag_method}(arg, :"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(arg, :"foo_\#{bar}_baz")|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(arg, :"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
- include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(arg, 123)|
+ include_examples 'does not set any flags as used', %|#{feature_flag_method}(arg, 123)|
end
end
end
diff --git a/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb b/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb
index fde53f8f98c..75455a390f4 100644
--- a/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb
+++ b/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb
@@ -100,4 +100,42 @@ RSpec.describe RuboCop::Cop::Gitlab::StrongMemoizeAttr do
RUBY
end
end
+
+ context 'when strong_memoize_with() is called without parameters' do
+ it 'registers an offense and autocorrects' do
+ expect_offense(<<~RUBY)
+ class Foo
+ def memoized_method
+ strong_memoize_with(:memoized_method) do
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `strong_memoize_attr`, instead of using `strong_memoize_with` without parameters.
+ 'This is a memoized method'
+ end
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def memoized_method
+ 'This is a memoized method'
+ end
+ strong_memoize_attr :memoized_method
+ end
+ RUBY
+ end
+ end
+
+ context 'when strong_memoize_with() is called with parameters' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ class Foo
+ def memoized_method(param)
+ strong_memoize_with(:memoized_method, param) do
+ param.to_s
+ end
+ end
+ end
+ RUBY
+ end
+ end
end
diff --git a/spec/rubocop/cop/graphql/gid_expected_type_spec.rb b/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
deleted file mode 100644
index 563c16a99df..00000000000
--- a/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop_spec_helper'
-
-require_relative '../../../../rubocop/cop/graphql/gid_expected_type'
-
-RSpec.describe RuboCop::Cop::Graphql::GIDExpectedType do
- it 'adds an offense when there is no expected_type parameter' do
- expect_offense(<<~TYPE)
- GitlabSchema.object_from_id(received_id)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Add an expected_type parameter to #object_from_id calls if possible.
- TYPE
- end
-
- it 'does not add an offense for calls that have an expected_type parameter' do
- expect_no_offenses(<<~TYPE.strip)
- GitlabSchema.object_from_id("some_id", expected_type: SomeClass)
- TYPE
- end
-end
diff --git a/spec/rubocop/cop/graphql/id_type_spec.rb b/spec/rubocop/cop/graphql/id_type_spec.rb
index 3a56753d39e..6eb4890c064 100644
--- a/spec/rubocop/cop/graphql/id_type_spec.rb
+++ b/spec/rubocop/cop/graphql/id_type_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe RuboCop::Cop::Graphql::IDType do
TYPE
end
- context 'whitelisted arguments' do
- RuboCop::Cop::Graphql::IDType::WHITELISTED_ARGUMENTS.each do |arg|
+ context 'allowlisted arguments' do
+ RuboCop::Cop::Graphql::IDType::ALLOWLISTED_ARGUMENTS.each do |arg|
it "does not add an offense for calls to #argument with #{arg} as argument name" do
expect_no_offenses(<<~TYPE.strip)
argument #{arg}, GraphQL::Types::ID, some: other, params: do_not_matter
diff --git a/spec/rubocop/cop/ignored_columns_spec.rb b/spec/rubocop/cop/ignored_columns_spec.rb
index 8d2c6b92c70..c8f47f8aee9 100644
--- a/spec/rubocop/cop/ignored_columns_spec.rb
+++ b/spec/rubocop/cop/ignored_columns_spec.rb
@@ -4,20 +4,20 @@ require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/ignored_columns'
RSpec.describe RuboCop::Cop::IgnoredColumns, feature_category: :database do
- it 'flags use of `self.ignored_columns +=` instead of the IgnoredColumns concern' do
+ it 'flags use of `self.ignored_columns +=` instead of the IgnorableColumns concern' do
expect_offense(<<~RUBY)
class Foo < ApplicationRecord
self.ignored_columns += %i[id]
- ^^^^^^^^^^^^^^^ Use `IgnoredColumns` concern instead of adding to `self.ignored_columns`.
+ ^^^^^^^^^^^^^^^ Use `IgnorableColumns` concern instead of adding to `self.ignored_columns`.
end
RUBY
end
- it 'flags use of `self.ignored_columns =` instead of the IgnoredColumns concern' do
+ it 'flags use of `self.ignored_columns =` instead of the IgnorableColumns concern' do
expect_offense(<<~RUBY)
class Foo < ApplicationRecord
self.ignored_columns = %i[id]
- ^^^^^^^^^^^^^^^ Use `IgnoredColumns` concern instead of setting `self.ignored_columns`.
+ ^^^^^^^^^^^^^^^ Use `IgnorableColumns` concern instead of setting `self.ignored_columns`.
end
RUBY
end
diff --git a/spec/rubocop/cop/migration/avoid_finalize_background_migration_spec.rb b/spec/rubocop/cop/migration/avoid_finalize_background_migration_spec.rb
new file mode 100644
index 00000000000..e4eec39e3ff
--- /dev/null
+++ b/spec/rubocop/cop/migration/avoid_finalize_background_migration_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/migration/avoid_finalize_background_migration'
+
+RSpec.describe RuboCop::Cop::Migration::AvoidFinalizeBackgroundMigration, feature_category: :database do
+ context 'when file is under db/post_migration' do
+ it "flags the use of 'finalize_background_migration' method" do
+ expect_offense(<<~RUBY)
+ class FinalizeMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ finalize_background_migration(MIGRATION)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ end
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/rubocop/cop/qa/element_with_pattern_spec.rb b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
index 1febdaf9c3b..ccc03d7f5ae 100644
--- a/spec/rubocop/cop/qa/element_with_pattern_spec.rb
+++ b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe RuboCop::Cop::QA::ElementWithPattern do
end
end
- context 'outside of a migration spec file' do
+ context 'when outside of a QA spec file' do
it "does not register an offense" do
expect_no_offenses(<<-RUBY)
describe 'foo' do
diff --git a/spec/rubocop/cop/rake/require_spec.rb b/spec/rubocop/cop/rake/require_spec.rb
index bb8c6a1f063..de484643d6e 100644
--- a/spec/rubocop/cop/rake/require_spec.rb
+++ b/spec/rubocop/cop/rake/require_spec.rb
@@ -7,54 +7,84 @@ require_relative '../../../../rubocop/cop/rake/require'
RSpec.describe RuboCop::Cop::Rake::Require do
let(:msg) { described_class::MSG }
- it 'registers an offenses for require methods' do
- expect_offense(<<~RUBY)
- require 'json'
- ^^^^^^^^^^^^^^ #{msg}
- require_relative 'gitlab/json'
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
- RUBY
+ describe '#in_rake_file?' do
+ context 'in a Rake file' do
+ let(:node) { double(location: double(expression: double(source_buffer: double(name: 'foo/bar.rake')))) } # rubocop:disable RSpec/VerifiedDoubles
+
+ it { expect(subject.__send__(:in_rake_file?, node)).to be(true) }
+ end
+
+ context 'when outside of a Rake file' do
+ let(:node) { double(location: double(expression: double(source_buffer: double(name: 'foo/bar.rb')))) } # rubocop:disable RSpec/VerifiedDoubles
+
+ it { expect(subject.__send__(:in_rake_file?, node)).to be(false) }
+ end
end
- it 'does not register offense inside `task` definition' do
- expect_no_offenses(<<~RUBY)
- task :parse do
+ context 'in a Rake file' do
+ before do
+ allow(cop).to receive(:in_rake_file?).and_return(true)
+ end
+
+ it 'registers an offenses for require methods' do
+ expect_offense(<<~RUBY)
require 'json'
- end
+ ^^^^^^^^^^^^^^ #{msg}
+ require_relative 'gitlab/json'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ RUBY
+ end
- namespace :some do
- task parse: :env do
- require_relative 'gitlab/json'
+ it 'does not register offense inside `task` definition' do
+ expect_no_offenses(<<~RUBY)
+ task :parse do
+ require 'json'
end
- end
- RUBY
- end
- it 'does not register offense inside a block definition' do
- expect_no_offenses(<<~RUBY)
- RSpec::Core::RakeTask.new(:parse_json) do |t, args|
- require 'json'
- end
- RUBY
- end
+ namespace :some do
+ task parse: :env do
+ require_relative 'gitlab/json'
+ end
+ end
+ RUBY
+ end
- it 'does not register offense inside a method definition' do
- expect_no_offenses(<<~RUBY)
- def load_deps
- require 'json'
- end
+ it 'does not register offense inside a block definition' do
+ expect_no_offenses(<<~RUBY)
+ RSpec::Core::RakeTask.new(:parse_json) do |t, args|
+ require 'json'
+ end
+ RUBY
+ end
+
+ it 'does not register offense inside a method definition' do
+ expect_no_offenses(<<~RUBY)
+ def load_deps
+ require 'json'
+ end
- task :parse do
- load_deps
- end
- RUBY
+ task :parse do
+ load_deps
+ end
+ RUBY
+ end
+
+ it 'does not register offense when require task related files' do
+ expect_no_offenses(<<~RUBY)
+ require 'rubocop/rake_tasks'
+ require 'gettext_i18n_rails/tasks'
+ require_relative '../../rubocop/check_graceful_task'
+ RUBY
+ end
end
- it 'does not register offense when require task related files' do
- expect_no_offenses(<<~RUBY)
- require 'rubocop/rake_tasks'
- require 'gettext_i18n_rails/tasks'
- require_relative '../../rubocop/check_graceful_task'
- RUBY
+ context 'when outside of a Rake file' do
+ before do
+ allow(cop).to receive(:in_rake_file?).and_return(false)
+ end
+
+ it 'registers an offenses for require methods' do
+ expect_no_offenses("require 'json'")
+ end
end
end
diff --git a/spec/rubocop/cop/rspec/before_all_role_assignment_spec.rb b/spec/rubocop/cop/rspec/before_all_role_assignment_spec.rb
new file mode 100644
index 00000000000..f8a9a6e22c4
--- /dev/null
+++ b/spec/rubocop/cop/rspec/before_all_role_assignment_spec.rb
@@ -0,0 +1,234 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require 'rspec-parameterized'
+require_relative '../../../../rubocop/cop/rspec/before_all_role_assignment'
+
+RSpec.describe Rubocop::Cop::RSpec::BeforeAllRoleAssignment, :rubocop_rspec, feature_category: :tooling do
+ context 'with `let`' do
+ context 'and `before_all`' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ context 'with something' do
+ let(:project) { create(:project) }
+ let(:guest) { create(:user) }
+
+ before_all do
+ project.add_guest(guest)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'and `before`' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ context 'with something' do
+ let(:project) { create(:project) }
+ let(:guest) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ shared_examples '`let_it_be` definitions' do |let_it_be|
+ context 'and `before_all`' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ context 'with something' do
+ #{let_it_be}(:project) { create(:project) }
+ #{let_it_be}(:guest) { create(:user) }
+
+ before_all do
+ project.add_guest(guest)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'and `before`' do
+ context 'and without role methods' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ context 'with something' do
+ #{let_it_be}(:project) { create(:project) }
+ #{let_it_be}(:guest) { create(:user) }
+
+ before do
+ project.add_details(guest)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'and role methods' do
+ where(:role_method) { described_class::ROLE_METHODS.to_a }
+
+ with_them do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY, role_method: role_method)
+ context 'with something' do
+ #{let_it_be}(:project) { create(:project) }
+ #{let_it_be}(:guest) { create(:user) }
+
+ before do
+ project.%{role_method}(guest)
+ ^^^^^^^^^{role_method}^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'without nested contexts' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ context 'with something' do
+ #{let_it_be}(:project) { create(:project) }
+ #{let_it_be}(:guest) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'with nested contexts' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ context 'when first context' do
+ #{let_it_be}(:guest) { create(:user) }
+
+ context 'when second context' do
+ #{let_it_be}(:project) { create(:project) }
+
+ context 'when third context' do
+ before do
+ project.add_guest(guest)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ describe 'edge cases' do
+ context 'with unrelated `let_it_be` definition' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ context 'with something' do
+ let(:project) { create(:project) }
+ #{let_it_be}(:user) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'with many role method calls' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ context 'with something' do
+ let(:project) { create(:project) }
+ #{let_it_be}(:other_project) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ other_project.add_guest(guest)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'with alternative example groups' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ describe 'with something' do
+ #{let_it_be}(:project) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+
+ it_behaves_like 'with something' do
+ #{let_it_be}(:project) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+
+ include_examples 'with something' do
+ #{let_it_be}(:project) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use `before_all` when used with `#{let_it_be}`.
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'with `let_it_be` outside of the ancestors chain' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ context 'when in main context' do
+ let(:project) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ context 'when in a separate context' do
+ #{let_it_be}(:project) { create(:user) }
+
+ before do
+ project
+ end
+ end
+ end
+ RUBY
+ end
+ end
+ end
+ end
+ end
+
+ context 'with `let_it_be` variants' do
+ before do
+ other_cops.tap do |config|
+ config.dig('RSpec', 'Language', 'Helpers')
+ .push('let_it_be', 'let_it_be_with_reload', 'let_it_be_with_refind')
+ end
+ end
+
+ where(:let_it_be) { %i[let_it_be let_it_be_with_reload let_it_be_with_refind] }
+
+ with_them do
+ include_examples '`let_it_be` definitions', params[:let_it_be]
+ end
+ end
+end
diff --git a/spec/rubocop/cop/search/avoid_checking_finished_on_deprecated_migrations_spec.rb b/spec/rubocop/cop/search/avoid_checking_finished_on_deprecated_migrations_spec.rb
new file mode 100644
index 00000000000..9853423e758
--- /dev/null
+++ b/spec/rubocop/cop/search/avoid_checking_finished_on_deprecated_migrations_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/search/avoid_checking_finished_on_deprecated_migrations'
+
+RSpec.describe RuboCop::Cop::Search::AvoidCheckingFinishedOnDeprecatedMigrations, feature_category: :global_search do
+ context 'when a deprecated class is used with migration_has_finished?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return if Elastic::DataMigrationService.migration_has_finished?(:backfill_project_permissions_in_blobs_using_permutations)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Migration is deprecated and can not be used with `migration_has_finished?`.
+ SOURCE
+ end
+ end
+
+ context 'when a non deprecated class is used with migration_has_finished?' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ return if Elastic::DataMigrationService.migration_has_finished?(:backfill_project_permissions_in_blobs)
+ SOURCE
+ end
+ end
+
+ context 'when migration_has_finished? method is called on another class' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ return if Klass.migration_has_finished?(:backfill_project_permissions_in_blobs_using_permutations)
+ SOURCE
+ end
+ end
+end
diff --git a/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb b/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
index b4d113a9bcc..53bf5de6243 100644
--- a/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
+++ b/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
@@ -13,33 +13,45 @@ RSpec.describe RuboCop::Cop::UsageData::DistinctCountByLargeForeignKey do
})
end
- context 'when counting by disallowed key' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- distinct_count(Issue, :creator_id)
- ^^^^^^^^^^^^^^ #{msg}
- CODE
+ context 'in an usage data file' do
+ before do
+ allow(cop).to receive(:in_usage_data_file?).and_return(true)
end
- it 'does not register an offense when batch is false' do
- expect_no_offenses('distinct_count(Issue, :creator_id, batch: false)')
+ context 'when counting by disallowed key' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ distinct_count(Issue, :creator_id)
+ ^^^^^^^^^^^^^^ #{msg}
+ CODE
+ end
+
+ it 'does not register an offense when batch is false' do
+ expect_no_offenses('distinct_count(Issue, :creator_id, batch: false)')
+ end
+
+ it 'registers an offense when batch is true' do
+ expect_offense(<<~CODE)
+ distinct_count(Issue, :creator_id, batch: true)
+ ^^^^^^^^^^^^^^ #{msg}
+ CODE
+ end
end
- it 'registers an offense when batch is true' do
- expect_offense(<<~CODE)
- distinct_count(Issue, :creator_id, batch: true)
- ^^^^^^^^^^^^^^ #{msg}
- CODE
- end
- end
+ context 'when calling by allowed key' do
+ it 'does not register an offense with symbol' do
+ expect_no_offenses('distinct_count(Issue, :author_id)')
+ end
- context 'when calling by allowed key' do
- it 'does not register an offense with symbol' do
- expect_no_offenses('distinct_count(Issue, :author_id)')
+ it 'does not register an offense with string' do
+ expect_no_offenses("distinct_count(Issue, 'merge_requests.target_project_id')")
+ end
end
+ end
- it 'does not register an offense with string' do
- expect_no_offenses("distinct_count(Issue, 'merge_requests.target_project_id')")
+ context 'when outside of an usage data file' do
+ it 'does not register an offense' do
+ expect_no_offenses('distinct_count(Issue, :creator_id)')
end
end
end
diff --git a/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb b/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
index efa4e27dc9c..0de14310e13 100644
--- a/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
+++ b/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
@@ -14,93 +14,105 @@ RSpec.describe RuboCop::Cop::UsageData::HistogramWithLargeTable do
})
end
- context 'with large tables' do
- context 'with one-level constants' do
- context 'when calling histogram(Issue)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(Issue, :project_id, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
- CODE
+ context 'in an usage data file' do
+ before do
+ allow(cop).to receive(:in_usage_data_file?).and_return(true)
+ end
+
+ context 'with large tables' do
+ context 'with one-level constants' do
+ context 'when calling histogram(Issue)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Issue, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
end
- end
- context 'when calling histogram(::Issue)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(::Issue, :project_id, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
- CODE
+ context 'when calling histogram(::Issue)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Issue, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
end
- end
- context 'when calling histogram(Issue.closed)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(Issue.closed, :project_id, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
- CODE
+ context 'when calling histogram(Issue.closed)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Issue.closed, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
end
- end
- context 'when calling histogram(::Issue.closed)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(::Issue.closed, :project_id, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
- CODE
+ context 'when calling histogram(::Issue.closed)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Issue.closed, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
end
end
- end
- context 'with two-level constants' do
- context 'when calling histogram(::Ci::Build)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(::Ci::Build, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
- CODE
+ context 'with two-level constants' do
+ context 'when calling histogram(::Ci::Build)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Ci::Build, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
end
- end
- context 'when calling histogram(::Ci::Build.active)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(::Ci::Build.active, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
- CODE
+ context 'when calling histogram(::Ci::Build.active)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Ci::Build.active, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
end
- end
- context 'when calling histogram(Ci::Build)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(Ci::Build, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
- CODE
+ context 'when calling histogram(Ci::Build)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Ci::Build, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
end
- end
- context 'when calling histogram(Ci::Build.active)' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- histogram(Ci::Build.active, buckets: 1..100)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
- CODE
+ context 'when calling histogram(Ci::Build.active)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Ci::Build.active, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
end
end
end
- end
- context 'with non related class' do
- it 'does not register an offense' do
- expect_no_offenses('histogram(MergeRequest, buckets: 1..100)')
+ context 'with non related class' do
+ it 'does not register an offense' do
+ expect_no_offenses('histogram(MergeRequest, buckets: 1..100)')
+ end
+ end
+
+ context 'with non related method' do
+ it 'does not register an offense' do
+ expect_no_offenses('count(Issue, buckets: 1..100)')
+ end
end
end
- context 'with non related method' do
+ context 'when outside of an usage data file' do
it 'does not register an offense' do
- expect_no_offenses('count(Issue, buckets: 1..100)')
+ expect_no_offenses('histogram(Issue, :project_id, buckets: 1..100)')
end
end
end
diff --git a/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb b/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
index a55f0852f35..f208a5451cb 100644
--- a/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
+++ b/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
@@ -14,49 +14,63 @@ RSpec.describe RuboCop::Cop::UsageData::InstrumentationSuperclass do
})
end
- context 'with class definition' do
- context 'when inheriting from allowed superclass' do
- it 'does not register an offense' do
- expect_no_offenses('class NewMetric < GenericMetric; end')
- end
+ context 'when in an instrumentation file' do
+ before do
+ allow(cop).to receive(:in_instrumentation_file?).and_return(true)
end
- context 'when inheriting from some other superclass' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- class NewMetric < BaseMetric; end
- ^^^^^^^^^^ #{msg}
- CODE
+ context 'with class definition' do
+ context 'when inheriting from allowed superclass' do
+ it 'does not register an offense' do
+ expect_no_offenses('class NewMetric < GenericMetric; end')
+ end
end
- end
- context 'when not inheriting' do
- it 'does not register an offense' do
- expect_no_offenses('class NewMetric; end')
+ context 'when inheriting from some other superclass' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ class NewMetric < BaseMetric; end
+ ^^^^^^^^^^ #{msg}
+ CODE
+ end
end
- end
- end
- context 'with dynamic class definition' do
- context 'when inheriting from allowed superclass' do
- it 'does not register an offense' do
- expect_no_offenses('NewMetric = Class.new(GenericMetric)')
+ context 'when not inheriting' do
+ it 'does not register an offense' do
+ expect_no_offenses('class NewMetric; end')
+ end
end
end
- context 'when inheriting from some other superclass' do
- it 'registers an offense' do
- expect_offense(<<~CODE)
- NewMetric = Class.new(BaseMetric)
- ^^^^^^^^^^ #{msg}
- CODE
+ context 'with dynamic class definition' do
+ context 'when inheriting from allowed superclass' do
+ it 'does not register an offense' do
+ expect_no_offenses('NewMetric = Class.new(GenericMetric)')
+ end
end
- end
- context 'when not inheriting' do
- it 'does not register an offense' do
- expect_no_offenses('NewMetric = Class.new')
+ context 'when inheriting from some other superclass' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ NewMetric = Class.new(BaseMetric)
+ ^^^^^^^^^^ #{msg}
+ CODE
+ end
end
+
+ context 'when not inheriting' do
+ it 'does not register an offense' do
+ expect_no_offenses('NewMetric = Class.new')
+ end
+ end
+ end
+ end
+
+ context 'when outside of an instrumentation file' do
+ it "does not register an offense" do
+ expect_no_offenses(<<-RUBY)
+ class NewMetric < BaseMetric; end
+ RUBY
end
end
end
diff --git a/spec/rubocop/cop/usage_data/large_table_spec.rb b/spec/rubocop/cop/usage_data/large_table_spec.rb
index fa94f878cea..ceeb1143690 100644
--- a/spec/rubocop/cop/usage_data/large_table_spec.rb
+++ b/spec/rubocop/cop/usage_data/large_table_spec.rb
@@ -18,9 +18,9 @@ RSpec.describe RuboCop::Cop::UsageData::LargeTable do
})
end
- context 'when in usage_data files' do
+ context 'in an usage data file' do
before do
- allow(cop).to receive(:usage_data_files?).and_return(true)
+ allow(cop).to receive(:in_usage_data_file?).and_return(true)
end
context 'with large tables' do
@@ -76,4 +76,10 @@ RSpec.describe RuboCop::Cop::UsageData::LargeTable do
end
end
end
+
+ context 'when outside of an usage data file' do
+ it 'does not register an offense' do
+ expect_no_offenses('Issue.active.count')
+ end
+ end
end
diff --git a/spec/rubocop/cop_todo_spec.rb b/spec/rubocop/cop_todo_spec.rb
index c641001789f..49206d76d5a 100644
--- a/spec/rubocop/cop_todo_spec.rb
+++ b/spec/rubocop/cop_todo_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../rubocop/cop_todo'
-RSpec.describe RuboCop::CopTodo do
+RSpec.describe RuboCop::CopTodo, feature_category: :tooling do
let(:cop_name) { 'Cop/Rule' }
subject(:cop_todo) { described_class.new(cop_name) }
@@ -32,6 +32,19 @@ RSpec.describe RuboCop::CopTodo do
end
end
+ describe '#add_files' do
+ it 'adds files' do
+ cop_todo.add_files(%w[a.rb b.rb])
+ cop_todo.add_files(%w[a.rb])
+ cop_todo.add_files(%w[])
+
+ expect(cop_todo).to have_attributes(
+ files: contain_exactly('a.rb', 'b.rb'),
+ offense_count: 0
+ )
+ end
+ end
+
describe '#autocorrectable?' do
subject { cop_todo.autocorrectable? }
diff --git a/spec/rubocop/formatter/todo_formatter_spec.rb b/spec/rubocop/formatter/todo_formatter_spec.rb
index 5494d518605..55a64198289 100644
--- a/spec/rubocop/formatter/todo_formatter_spec.rb
+++ b/spec/rubocop/formatter/todo_formatter_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
# rubocop:disable RSpec/VerifiedDoubles
require 'fast_spec_helper'
@@ -10,7 +11,7 @@ require 'tmpdir'
require_relative '../../../rubocop/formatter/todo_formatter'
require_relative '../../../rubocop/todo_dir'
-RSpec.describe RuboCop::Formatter::TodoFormatter do
+RSpec.describe RuboCop::Formatter::TodoFormatter, feature_category: :tooling do
let(:stdout) { StringIO.new }
let(:tmp_dir) { Dir.mktmpdir }
let(:real_tmp_dir) { File.join(tmp_dir, 'real') }
@@ -97,6 +98,40 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
YAML
end
+ context 'with existing HAML exclusions' do
+ before do
+ todo_dir.write('B/TooManyOffenses', <<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'd.rb'
+ - 'app/views/project.html.haml.rb'
+ - 'app/views/project.haml.rb'
+ - 'app/views/project.text.haml.rb'
+ - 'app/views/unrelated.html.haml.rb.ext'
+ - 'app/views/unrelated.html.haml.ext'
+ - 'app/views/unrelated.html.haml'
+ YAML
+
+ todo_dir.inspect_all
+ end
+
+ it 'does not remove them' do
+ run_formatter
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'a.rb'
+ - 'app/views/project.haml.rb'
+ - 'app/views/project.html.haml.rb'
+ - 'app/views/project.text.haml.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
context 'when cop previously not explicitly disabled' do
before do
todo_dir.write('B/TooManyOffenses', <<~YAML)
@@ -105,6 +140,8 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
Exclude:
- 'x.rb'
YAML
+
+ todo_dir.inspect_all
end
it 'does not disable cop' do
@@ -158,6 +195,8 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
Exclude:
- 'x.rb'
YAML
+
+ todo_dir.inspect_all
end
it 'keeps cop disabled' do
diff --git a/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb b/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb
index 79e63c95f65..7aff33855aa 100644
--- a/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb
+++ b/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'gitlab/rspec/all'
require_relative '../../scripts/generate-failed-package-and-test-mr-message'
-require_relative '../support/helpers/stub_env'
RSpec.describe GenerateFailedPackageAndTestMrMessage, feature_category: :tooling do
include StubENV
diff --git a/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb b/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
index aee16334003..61307937101 100644
--- a/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
+++ b/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
@@ -3,8 +3,8 @@
# rubocop:disable RSpec/VerifiedDoubles
require 'fast_spec_helper'
+require 'gitlab/rspec/all'
require_relative '../../scripts/generate-message-to-run-e2e-pipeline'
-require_relative '../support/helpers/stub_env'
RSpec.describe GenerateMessageToRunE2ePipeline, feature_category: :tooling do
include StubENV
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index 87b2c42c5b8..78ea31c8e39 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -312,7 +312,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
static: |-
<p>This is the manually modified static HTML which will be preserved</p>
wysiwyg: |-
- <p>This is the manually modified WYSIWYG HTML which will be preserved</p>
+ <p dir="auto">This is the manually modified WYSIWYG HTML which will be preserved</p>
YAML
end
@@ -631,33 +631,33 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
static: |-
<p data-sourcepos="1:1-1:8" dir="auto"><strong>bold</strong></p>
wysiwyg: |-
- <p><strong>bold</strong></p>
+ <p dir="auto"><strong>bold</strong></p>
02_01_00__inlines__strong__002:
canonical: |
<p><strong>bold with more text</strong></p>
static: |-
<p data-sourcepos="1:1-1:23" dir="auto"><strong>bold with more text</strong></p>
wysiwyg: |-
- <p><strong>bold with more text</strong></p>
+ <p dir="auto"><strong>bold with more text</strong></p>
02_03_00__inlines__strikethrough_extension__001:
canonical: |
<p><del>Hi</del> Hello, world!</p>
static: |-
<p data-sourcepos="1:1-1:20" dir="auto"><del>Hi</del> Hello, world!</p>
wysiwyg: |-
- <p><s>Hi</s> Hello, world!</p>
+ <p dir="auto"><s>Hi</s> Hello, world!</p>
03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
canonical: |
<p><strong>bold</strong></p>
wysiwyg: |-
- <p><strong>bold</strong></p>
+ <p dir="auto"><strong>bold</strong></p>
03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001:
canonical: |
<p>Example in an H3</p>
static: |-
<p data-sourcepos="1:1-1:16" dir="auto">Example in an H3</p>
wysiwyg: |-
- <p>Example in an H3</p>
+ <p dir="auto">Example in an H3</p>
04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
canonical: |
<p><strong>
@@ -673,42 +673,42 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
static: |-
<p>This is the manually modified static HTML which will be preserved</p>
wysiwyg: |-
- <p>This is the manually modified WYSIWYG HTML which will be preserved</p>
+ <p dir="auto">This is the manually modified WYSIWYG HTML which will be preserved</p>
06_01_00__api_request_overrides__group_upload_link__001:
canonical: |
<p><a href="groups-test-file">groups-test-file</a></p>
static: |-
<p data-sourcepos="1:1-1:45" dir="auto"><a href="/groups/glfm_group/-/uploads/groups-test-file" data-canonical-src="/uploads/groups-test-file" data-link="true" class="gfm">groups-test-file</a></p>
wysiwyg: |-
- <p><a target="_blank" rel="noopener noreferrer nofollow" href="/uploads/groups-test-file">groups-test-file</a></p>
+ <p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="/uploads/groups-test-file">groups-test-file</a></p>
06_02_00__api_request_overrides__project_repo_link__001:
canonical: |
<p><a href="projects-test-file">projects-test-file</a></p>
static: |-
<p data-sourcepos="1:1-1:40" dir="auto"><a href="/glfm_group/glfm_project/-/blob/master/projects-test-file" class="gfm">projects-test-file</a></p>
wysiwyg: |-
- <p><a target="_blank" rel="noopener noreferrer nofollow" href="projects-test-file">projects-test-file</a></p>
+ <p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="projects-test-file">projects-test-file</a></p>
06_03_00__api_request_overrides__project_snippet_ref__001:
canonical: |
<p>This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888">$88888</a>
static: |-
<p data-sourcepos="1:1-1:53" dir="auto">This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888" data-reference-type="snippet" data-original="$88888" data-link="false" data-link-reference="false" data-project="77777" data-snippet="88888" data-container="body" data-placement="top" title="glfm_project_snippet" class="gfm gfm-snippet has-tooltip">$88888</a></p>
wysiwyg: |-
- <p>This project snippet ID reference IS filtered: $88888</p>
+ <p dir="auto">This project snippet ID reference IS filtered: $88888</p>
06_04_00__api_request_overrides__personal_snippet_ref__001:
canonical: |
<p>This personal snippet ID reference is NOT filtered: $99999</p>
static: |-
<p data-sourcepos="1:1-1:58" dir="auto">This personal snippet ID reference is NOT filtered: $99999</p>
wysiwyg: |-
- <p>This personal snippet ID reference is NOT filtered: $99999</p>
+ <p dir="auto">This personal snippet ID reference is NOT filtered: $99999</p>
06_05_00__api_request_overrides__project_wiki_link__001:
canonical: |
<p><a href="project-wikis-test-file">project-wikis-test-file</a></p>
static: |-
<p data-sourcepos="1:1-1:50" dir="auto"><a href="/glfm_group/glfm_project/-/wikis/project-wikis-test-file" data-canonical-src="project-wikis-test-file">project-wikis-test-file</a></p>
wysiwyg: |-
- <p><a target="_blank" rel="noopener noreferrer nofollow" href="project-wikis-test-file">project-wikis-test-file</a></p>
+ <p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="project-wikis-test-file">project-wikis-test-file</a></p>
YAML
end
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index 78cc57b6c91..3ac383e8d30 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -211,6 +211,7 @@ RSpec.describe Trigger, feature_category: :tooling do
context "when set in a file" do
before do
allow(File).to receive(:read).and_call_original
+ stub_env(version_file, nil)
end
it 'includes the version from the file' do
diff --git a/spec/serializers/context_commits_diff_entity_spec.rb b/spec/serializers/context_commits_diff_entity_spec.rb
index e8f38527f5b..9d74fbc9160 100644
--- a/spec/serializers/context_commits_diff_entity_spec.rb
+++ b/spec/serializers/context_commits_diff_entity_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ContextCommitsDiffEntity do
let_it_be(:mrcc2) { create(:merge_request_context_commit, merge_request: merge_request, sha: "ae73cb07c9eeaf35924a10f713b364d32b2dd34f") }
context 'as json' do
- subject { ContextCommitsDiffEntity.represent(merge_request.context_commits_diff).as_json }
+ subject { described_class.represent(merge_request.context_commits_diff).as_json }
it 'exposes commits_count' do
expect(subject[:commits_count]).to eq(2)
diff --git a/spec/serializers/diff_viewer_entity_spec.rb b/spec/serializers/diff_viewer_entity_spec.rb
index 84d2bdceb78..7ee2f8ec12f 100644
--- a/spec/serializers/diff_viewer_entity_spec.rb
+++ b/spec/serializers/diff_viewer_entity_spec.rb
@@ -16,47 +16,27 @@ RSpec.describe DiffViewerEntity do
subject { described_class.new(viewer).as_json(options) }
- context 'when add_ignore_all_white_spaces is enabled' do
- before do
- stub_feature_flags(add_ignore_all_white_spaces: true)
- end
-
- it 'serializes diff file viewer' do
- expect(subject.with_indifferent_access).to match_schema('entities/diff_viewer')
- end
-
- it 'contains whitespace_only attribute' do
- expect(subject.with_indifferent_access).to include(:whitespace_only)
- end
-
- context 'when whitespace_only option is true' do
- let(:options) { { whitespace_only: true } }
+ it 'serializes diff file viewer' do
+ expect(subject.with_indifferent_access).to match_schema('entities/diff_viewer')
+ end
- it 'returns the whitespace_only attribute true' do
- expect(subject.with_indifferent_access[:whitespace_only]).to eq true
- end
- end
+ it 'contains whitespace_only attribute' do
+ expect(subject.with_indifferent_access).to include(:whitespace_only)
+ end
- context 'when whitespace_only option is false' do
- let(:options) { { whitespace_only: false } }
+ context 'when whitespace_only option is true' do
+ let(:options) { { whitespace_only: true } }
- it 'returns the whitespace_only attribute false' do
- expect(subject.with_indifferent_access[:whitespace_only]).to eq false
- end
+ it 'returns the whitespace_only attribute true' do
+ expect(subject.with_indifferent_access[:whitespace_only]).to eq true
end
end
- context 'when add_ignore_all_white_spaces is disabled ' do
- before do
- stub_feature_flags(add_ignore_all_white_spaces: false)
- end
-
- it 'serializes diff file viewer' do
- expect(subject.with_indifferent_access).to match_schema('entities/diff_viewer')
- end
+ context 'when whitespace_only option is false' do
+ let(:options) { { whitespace_only: false } }
- it 'does not contain whitespace_only attribute' do
- expect(subject.with_indifferent_access).not_to include(:whitespace_only)
+ it 'returns the whitespace_only attribute false' do
+ expect(subject.with_indifferent_access[:whitespace_only]).to eq false
end
end
end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index c60bead12c2..551acd2860d 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -83,32 +83,6 @@ RSpec.describe EnvironmentEntity do
end
end
- context 'when metrics dashboard feature is available' do
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- end
-
- context 'metrics disabled' do
- before do
- allow(environment).to receive(:has_metrics?).and_return(false)
- end
-
- it "doesn't expose metrics path" do
- expect(subject).not_to include(:metrics_path)
- end
- end
-
- context 'metrics enabled' do
- before do
- allow(environment).to receive(:has_metrics?).and_return(true)
- end
-
- it 'exposes metrics path' do
- expect(subject).to include(:metrics_path)
- end
- end
- end
-
it "doesn't expose metrics path" do
expect(subject).not_to include(:metrics_path)
end
diff --git a/spec/serializers/issue_sidebar_basic_entity_spec.rb b/spec/serializers/issue_sidebar_basic_entity_spec.rb
index d81d87f4060..ef80bcd5eb1 100644
--- a/spec/serializers/issue_sidebar_basic_entity_spec.rb
+++ b/spec/serializers/issue_sidebar_basic_entity_spec.rb
@@ -45,7 +45,6 @@ RSpec.describe IssueSidebarBasicEntity do
context 'for an incident issue' do
before do
issue.update!(
- issue_type: WorkItems::Type.base_types[:incident],
work_item_type: WorkItems::Type.default_by_type(:incident)
)
end
diff --git a/spec/serializers/lfs_file_lock_entity_spec.rb b/spec/serializers/lfs_file_lock_entity_spec.rb
index 5869941c920..d0ff6fb60af 100644
--- a/spec/serializers/lfs_file_lock_entity_spec.rb
+++ b/spec/serializers/lfs_file_lock_entity_spec.rb
@@ -16,6 +16,6 @@ RSpec.describe LfsFileLockEntity do
it 'exposes the owner info' do
expect(subject).to include(:owner)
- expect(subject[:owner][:name]).to eq(user.name)
+ expect(subject[:owner][:name]).to eq(user.username)
end
end
diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb
index 726f35418a1..6b80609c348 100644
--- a/spec/serializers/merge_request_poll_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe MergeRequestPollWidgetEntity do
end
end
- context 'when head pipeline is running' do
+ context 'when head pipeline is running', unless: Gitlab.ee? do
before do
create(:ci_pipeline, :running, project: project, ref: resource.source_branch, sha: resource.diff_head_sha)
resource.update_head_pipeline
diff --git a/spec/serializers/prometheus_alert_entity_spec.rb b/spec/serializers/prometheus_alert_entity_spec.rb
deleted file mode 100644
index 02da5a5bb88..00000000000
--- a/spec/serializers/prometheus_alert_entity_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PrometheusAlertEntity do
- let(:user) { build_stubbed(:user) }
- let(:prometheus_alert) { build_stubbed(:prometheus_alert) }
- let(:request) { double('prometheus_alert', current_user: user) }
- let(:entity) { described_class.new(prometheus_alert, request: request) }
-
- subject { entity.as_json }
-
- context 'when user can read prometheus alerts' do
- before do
- prometheus_alert.project.add_maintainer(user)
- end
-
- it 'exposes prometheus_alert attributes' do
- expect(subject).to include(:id, :title, :query, :operator, :threshold, :runbook_url)
- end
- end
-end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 5cb5724ebdc..fe8ee027245 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe StageEntity do
+RSpec.describe StageEntity, feature_category: :continuous_integration do
let(:pipeline) { create(:ci_pipeline) }
let(:request) { double('request') }
let(:user) { create(:user) }
@@ -76,8 +76,8 @@ RSpec.describe StageEntity do
context 'with a skipped stage ' do
let(:stage) { create(:ci_stage, status: 'skipped') }
- it 'contains play_all_manual' do
- expect(subject[:status][:action]).to be_present
+ it 'does not contain play_all_manual' do
+ expect(subject[:status][:action]).not_to be_present
end
end
diff --git a/spec/services/admin/plan_limits/update_service_spec.rb b/spec/services/admin/plan_limits/update_service_spec.rb
index 4a384b98299..718367fadc2 100644
--- a/spec/services/admin/plan_limits/update_service_spec.rb
+++ b/spec/services/admin/plan_limits/update_service_spec.rb
@@ -33,12 +33,10 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
subject(:update_plan_limits) { described_class.new(params, current_user: user, plan: plan).execute }
context 'when current_user is an admin', :enable_admin_mode do
- context 'when the update is successful' do
- it 'updates all attributes' do
- expect_next_instance_of(described_class) do |instance|
- expect(instance).to receive(:parsed_params).and_call_original
- end
+ context 'when the update is successful', :freeze_time do
+ let(:current_timestamp) { Time.current.utc.to_i }
+ it 'updates all attributes' do
update_plan_limits
params.each do |key, value|
@@ -46,6 +44,22 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do
end
end
+ it 'logs the allowed attributes only' do
+ update_plan_limits
+
+ expect(limits.limits_history).to eq(
+ { "enforcement_limit" =>
+ [{ "user_id" => user.id, "username" => user.username,
+ "timestamp" => current_timestamp, "value" => 15 }],
+ "notification_limit" =>
+ [{ "user_id" => user.id, "username" => user.username,
+ "timestamp" => current_timestamp, "value" => 30 }],
+ "storage_size_limit" =>
+ [{ "user_id" => user.id, "username" => user.username,
+ "timestamp" => current_timestamp, "value" => 90 }] }
+ )
+ end
+
it 'returns success' do
response = update_plan_limits
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 fd81c0893ed..c883466cf25 100644
--- a/spec/services/alert_management/alerts/todo/create_service_spec.rb
+++ b/spec/services/alert_management/alerts/todo/create_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe AlertManagement::Alerts::Todo::CreateService, feature_category: :
let(:current_user) { user }
describe '#execute' do
- subject(:result) { AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute }
+ subject(:result) { described_class.new(alert, current_user).execute }
shared_examples 'permissions error' do
it 'returns an error', :aggregate_failures do
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 79d4fc67538..a05219a0a49 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -314,6 +314,15 @@ RSpec.describe ApplicationSettings::UpdateService do
end
end
+ context 'when default_branch_protection is updated' do
+ let(:expected) { ::Gitlab::Access::BranchProtection.protected_against_developer_pushes.stringify_keys }
+ let(:params) { { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
+
+ it "updates default_branch_protection_defaults from the default_branch_protection param" do
+ expect { subject.execute }.to change { application_settings.default_branch_protection_defaults }.from({}).to(expected)
+ end
+ end
+
context 'when protected path settings are passed' do
let(:params) do
{
diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb
index 8f92fbe272c..3ef9c8fc96e 100644
--- a/spec/services/auth/dependency_proxy_authentication_service_spec.rb
+++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :dependency_proxy do
let_it_be(:user) { create(:user) }
- let(:service) { Auth::DependencyProxyAuthenticationService.new(nil, user) }
+ let(:service) { described_class.new(nil, user) }
before do
stub_config(dependency_proxy: { enabled: true })
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 a0b22267960..79c931990bb 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
@@ -3,30 +3,7 @@
require 'spec_helper'
RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService, feature_category: :code_review_workflow do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :repository) }
-
- let(:mr_merge_if_green_enabled) do
- create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
- source_branch: "master", target_branch: 'feature',
- source_project: project, target_project: project, state: "opened")
- end
-
- let(:pipeline) do
- create(:ci_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project)
- end
-
- let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message')
- end
-
- before_all do
- project.add_maintainer(user)
- end
-
- before do
- allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
- end
+ include_context 'for auto_merge strategy context'
describe "#available_for?" do
subject { service.available_for?(mr_merge_if_green_enabled) }
@@ -64,152 +41,24 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService, feature_category: :c
end
describe "#execute" do
- let(:merge_request) do
- create(:merge_request, target_project: project, source_project: project,
- source_branch: "feature", target_branch: 'master')
- end
-
- context 'first time enabling' do
- before do
- allow(merge_request)
- .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
- expect(MailScheduler::NotificationServiceWorker).to receive(:perform_async).with('merge_when_pipeline_succeeds', merge_request, user).once
-
- service.execute(merge_request)
- end
-
- it 'sets the params, merge_user, and flag' do
- expect(merge_request).to be_valid
- expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
- expect(merge_request.merge_params).to include 'commit_message' => 'Awesome message'
- expect(merge_request.merge_user).to be user
- expect(merge_request.auto_merge_strategy).to eq AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
- end
-
- it 'creates a system note' do
- pipeline = build(:ci_pipeline)
- allow(merge_request).to receive(:actual_head_pipeline) { pipeline }
-
- note = merge_request.notes.last
- expect(note.note).to match "enabled an automatic merge when the pipeline for #{pipeline.sha}"
- end
- end
-
- context 'already approved' do
- let(:service) { described_class.new(project, user, should_remove_source_branch: true) }
- let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
-
- before do
- allow(mr_merge_if_green_enabled)
- .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
-
- allow(mr_merge_if_green_enabled).to receive(:mergeable?)
- .and_return(true)
-
- allow(pipeline).to receive(:success?).and_return(true)
- end
-
- it 'updates the merge params' do
- expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds)
- expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async).with('merge_when_pipeline_succeeds', any_args)
-
- service.execute(mr_merge_if_green_enabled)
- expect(mr_merge_if_green_enabled.merge_params).to have_key('should_remove_source_branch')
+ it_behaves_like 'auto_merge service #execute', 'merge_when_pipeline_succeeds' do
+ let(:auto_merge_strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+ let(:expected_note) do
+ "enabled an automatic merge when the pipeline for #{pipeline.sha}"
end
end
end
describe "#process" do
- let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
- let(:merge_request_head) do
- project.commit(mr_merge_if_green_enabled.source_branch).id
- end
-
- context 'when triggered by pipeline with valid ref and sha' do
- let(:triggering_pipeline) do
- create(:ci_pipeline, project: project, ref: merge_request_ref,
- sha: merge_request_head, status: 'success',
- head_pipeline_of: mr_merge_if_green_enabled)
- end
-
- it "merges all merge requests with merge when the pipeline succeeds enabled" do
- allow(mr_merge_if_green_enabled)
- .to receive_messages(head_pipeline: triggering_pipeline, actual_head_pipeline: triggering_pipeline)
-
- expect(MergeWorker).to receive(:perform_async)
- service.process(mr_merge_if_green_enabled)
- end
- end
-
- context 'when triggered by an old pipeline' do
- let(:old_pipeline) do
- create(:ci_pipeline, project: project, ref: merge_request_ref,
- sha: '1234abcdef', status: 'success')
- end
-
- it 'does not merge request' do
- expect(MergeWorker).not_to receive(:perform_async)
- service.process(mr_merge_if_green_enabled)
- end
- end
-
- context 'when triggered by pipeline from a different branch' do
- let(:unrelated_pipeline) do
- create(:ci_pipeline, project: project, ref: 'feature',
- sha: merge_request_head, status: 'success')
- end
-
- it 'does not merge request' do
- expect(MergeWorker).not_to receive(:perform_async)
- service.process(mr_merge_if_green_enabled)
- end
- end
-
- context 'when pipeline is merge request pipeline' do
- let(:pipeline) do
- create(:ci_pipeline, :success,
- source: :merge_request_event,
- ref: mr_merge_if_green_enabled.merge_ref_path,
- merge_request: mr_merge_if_green_enabled,
- merge_requests_as_head_pipeline: [mr_merge_if_green_enabled])
- end
-
- it 'merges the associated merge request' do
- allow(mr_merge_if_green_enabled)
- .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
-
- expect(MergeWorker).to receive(:perform_async)
- service.process(mr_merge_if_green_enabled)
- end
- end
+ it_behaves_like 'auto_merge service #process'
end
- describe "#cancel" do
- before do
- service.cancel(mr_merge_if_green_enabled)
- end
-
- it "resets all the pipeline succeeds params" do
- expect(mr_merge_if_green_enabled.merge_when_pipeline_succeeds).to be_falsey
- expect(mr_merge_if_green_enabled.merge_params).to eq({})
- expect(mr_merge_if_green_enabled.merge_user).to be nil
- end
-
- it 'posts a system note' do
- note = mr_merge_if_green_enabled.notes.last
- expect(note.note).to include 'canceled the automatic merge'
- end
+ describe '#cancel' do
+ it_behaves_like 'auto_merge service #cancel'
end
- describe "#abort" do
- before do
- service.abort(mr_merge_if_green_enabled, 'an error')
- end
-
- it 'posts a system note' do
- note = mr_merge_if_green_enabled.notes.last
- expect(note.note).to include 'aborted the automatic merge'
- end
+ describe '#abort' do
+ it_behaves_like 'auto_merge service #abort'
end
describe 'pipeline integration' do
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 94f4b414dca..64473884b13 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -17,10 +17,11 @@ RSpec.describe AutoMergeService, feature_category: :code_review_workflow do
it 'returns all strategies in preference order' do
if Gitlab.ee?
- is_expected.to eq(
- [AutoMergeService::STRATEGY_MERGE_TRAIN,
+ is_expected.to contain_exactly(
+ AutoMergeService::STRATEGY_MERGE_TRAIN,
AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS,
- AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS])
+ AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS,
+ AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
is_expected.to eq([AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS])
end
@@ -74,11 +75,15 @@ RSpec.describe AutoMergeService, feature_category: :code_review_workflow do
merge_request.update_head_pipeline
end
- it 'returns preferred strategy' do
+ it 'returns preferred strategy', if: Gitlab.ee? do
+ is_expected.to eq('merge_when_checks_pass')
+ end
+
+ it 'returns preferred strategy', unless: Gitlab.ee? do
is_expected.to eq('merge_when_pipeline_succeeds')
end
- context 'when the head piipeline succeeded' do
+ context 'when the head pipeline succeeded' do
let(:pipeline_status) { :success }
it 'returns available strategies' do
@@ -142,7 +147,11 @@ RSpec.describe AutoMergeService, feature_category: :code_review_workflow do
context 'when strategy is not specified' do
let(:strategy) {}
- it 'chooses the most preferred strategy' do
+ it 'chooses the most preferred strategy', if: Gitlab.ee? do
+ is_expected.to eq(:merge_when_checks_pass)
+ end
+
+ it 'chooses the most preferred strategy', unless: Gitlab.ee? do
is_expected.to eq(:merge_when_pipeline_succeeds)
end
end
diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb
index 99dbe6dc606..e90ea284f29 100644
--- a/spec/services/award_emojis/add_service_spec.rb
+++ b/spec/services/award_emojis/add_service_spec.rb
@@ -53,6 +53,12 @@ RSpec.describe AwardEmojis::AddService, feature_category: :team_planning do
expect(award.user).to eq(user)
end
+ it 'executes hooks' do
+ expect(service).to receive(:execute_hooks).with(kind_of(AwardEmoji), 'award')
+
+ service.execute
+ end
+
describe 'marking Todos as done' do
subject { service.execute }
diff --git a/spec/services/award_emojis/base_service_spec.rb b/spec/services/award_emojis/base_service_spec.rb
index f1ee4d1cfb8..0f67c619a48 100644
--- a/spec/services/award_emojis/base_service_spec.rb
+++ b/spec/services/award_emojis/base_service_spec.rb
@@ -3,15 +3,17 @@
require 'spec_helper'
RSpec.describe AwardEmojis::BaseService, feature_category: :team_planning do
- let(:awardable) { build(:note) }
- let(:current_user) { build(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:awardable) { create(:note, project: project) }
+
+ let(:emoji_name) { 'horse' }
describe '.initialize' do
subject { described_class }
it 'uses same emoji name if not an alias' do
- emoji_name = 'horse'
-
expect(subject.new(awardable, emoji_name, current_user).name).to eq(emoji_name)
end
@@ -22,4 +24,31 @@ RSpec.describe AwardEmojis::BaseService, feature_category: :team_planning do
expect(subject.new(awardable, emoji_alias, current_user).name).to eq(emoji_name)
end
end
+
+ describe '.execute_hooks' do
+ let(:award_emoji) { create(:award_emoji, awardable: awardable) }
+ let(:action) { 'award' }
+
+ subject { described_class.new(awardable, emoji_name, current_user) }
+
+ context 'with no emoji hooks configured' do
+ it 'does not build hook_data' do
+ expect(Gitlab::DataBuilder::Emoji).not_to receive(:build)
+ expect(award_emoji.awardable.project).not_to receive(:execute_hooks)
+
+ subject.execute_hooks(award_emoji, action)
+ end
+ end
+
+ context 'with emoji hooks configured' do
+ it 'builds hook_data and calls execute_hooks for project' do
+ hook_data = {}
+ create(:project_hook, project: project, emoji_events: true)
+ expect(Gitlab::DataBuilder::Emoji).to receive(:build).and_return(hook_data)
+ expect(award_emoji.awardable.project).to receive(:execute_hooks).with(hook_data, :emoji_hooks)
+
+ subject.execute_hooks(award_emoji, action)
+ end
+ end
+ end
end
diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb
index 109bdbfa986..fbadee87f45 100644
--- a/spec/services/award_emojis/destroy_service_spec.rb
+++ b/spec/services/award_emojis/destroy_service_spec.rb
@@ -85,6 +85,12 @@ RSpec.describe AwardEmojis::DestroyService, feature_category: :team_planning do
expect(result[:award]).to eq(award_from_user)
expect(result[:award]).to be_destroyed
end
+
+ it 'executes hooks' do
+ expect(service).to receive(:execute_hooks).with(award_from_user, 'revoke')
+
+ service.execute
+ end
end
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 4089e9e6da0..c9d00f61a90 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -20,10 +20,10 @@ RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
let_it_be(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let_it_be(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let_it_be(:backlog) { create(:backlog_list, board: board) }
+ let_it_be(:backlog) { board.lists.backlog.first }
let_it_be(:list1) { create(:list, board: board, label: development, position: 0) }
let_it_be(:list2) { create(:list, board: board, label: testing, position: 1) }
- let_it_be(:closed) { create(:closed_list, board: board) }
+ let_it_be(:closed) { board.lists.closed.first }
let_it_be(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let_it_be(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2]) }
@@ -109,10 +109,10 @@ RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
let(:p2_project1) { create(:label, title: 'P2_project1', project: project1, priority: 2) }
let(:p3_project1) { create(:label, title: 'P3_project1', project: project1, priority: 3) }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { board.lists.backlog.first }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:closed) { create(:closed_list, board: board) }
+ let!(:closed) { board.lists.closed.first }
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2, p2_project]) }
@@ -145,7 +145,6 @@ RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
context 'when the group is an ancestor' do
let(:parent) { create(:group) }
let(:group) { create(:group, parent: parent) }
- let!(:backlog) { create(:backlog_list, board: board) }
let(:board) { create(:board, group: parent) }
before do
@@ -161,7 +160,6 @@ RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:board) { create(:board, project: project) }
- let_it_be(:backlog) { create(:backlog_list, board: board) }
let(:issue) { create(:issue, project: project, relative_position: nil) }
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 90b705e05c3..97bd5e13454 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Boards::Lists::ListService, feature_category: :team_planning do
RSpec.shared_examples 'FOSS lists only' do
context 'when board contains a non FOSS list' do
# This scenario may happen when there used to be an EE license and user downgraded
- let!(:backlog_list) { create_backlog_list(board) }
+ let_it_be(:backlog_list) { board.lists.backlog.first }
let_it_be(:milestone) { create(:milestone, group: group) }
let_it_be(:assignee_list) do
list = build(:list, board: board, user_id: user.id, list_type: List.list_types[:assignee], position: 0)
@@ -59,9 +59,5 @@ RSpec.describe Boards::Lists::ListService, feature_category: :team_planning do
it_behaves_like 'lists list service'
it_behaves_like 'FOSS lists only'
end
-
- def create_backlog_list(board)
- create(:backlog_list, board: board)
- end
end
end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 87112137675..a81d1487fab 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -452,42 +452,6 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job3'))
end
end
-
- context 'when the FF introduce_rules_with_needs is disabled' do
- before do
- stub_feature_flags(introduce_rules_with_needs: false)
- end
-
- context 'when the `$var` rule matches' do
- it 'creates a pipeline without overridden needs' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('job1', 'job2', 'job3', 'job4')
-
- expect(job1.needs).to be_empty
- expect(job2.needs).to be_empty
- expect(job3.needs).to be_empty
- expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job1'))
- end
- end
-
- context 'when the `$var` rule does not match' do
- let(:initialization_params) { base_initialization_params.merge(variables_attributes: variables_attributes) }
-
- let(:variables_attributes) do
- [{ key: 'var', secret_value: 'SOME_VAR' }]
- end
-
- it 'creates a pipeline without overridden needs' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('job1', 'job2', 'job3', 'job4')
-
- expect(job1.needs).to be_empty
- expect(job2.needs).to be_empty
- expect(job3.needs).to be_empty
- expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job1'))
- end
- end
- end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index f75c95c66f9..a28ede89cee 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -2035,7 +2035,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors)
- .to include 'content does not have a valid YAML syntax'
+ .to include 'mapping values are not allowed'
end
end
end
@@ -2172,7 +2172,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors)
- .to include 'content does not have a valid YAML syntax'
+ .to include 'mapping values are not allowed'
end
end
end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index c43f1e5264e..15f2cc0990c 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -1094,34 +1094,6 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category
process_pipeline
end
end
-
- context 'when FF `ci_reset_skipped_jobs_in_atomic_processing` is disabled' do
- before do
- stub_feature_flags(ci_reset_skipped_jobs_in_atomic_processing: false)
-
- process_pipeline # First pipeline processing
-
- # Change the manual jobs from stopped to alive status
- manual1.enqueue!
- manual2.enqueue!
-
- mock_play_jobs_during_processing([manual1, manual2])
- end
-
- it 'does not run ResetSkippedJobsService' do
- expect(Ci::ResetSkippedJobsService).not_to receive(:new)
-
- process_pipeline
-
- expect(all_builds_names_and_statuses).to eq(statuses_2)
- end
-
- it 'does not log event' do
- expect(Gitlab::AppJsonLogger).not_to receive(:info)
-
- process_pipeline
- end
- end
end
context 'when a bridge job has parallel:matrix config', :sidekiq_inline do
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_skips_rollback_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_skips_rollback_on_failure.yml
new file mode 100644
index 00000000000..591e66304eb
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_skips_rollback_on_failure.yml
@@ -0,0 +1,47 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [build, test]
+
+ rollback:
+ stage: deploy
+ script: exit 0
+ when: on_failure
+ needs: [build, test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+ rollback: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
+ rollback: skipped
diff --git a/spec/services/ci/pipeline_schedules/create_service_spec.rb b/spec/services/ci/pipeline_schedules/create_service_spec.rb
new file mode 100644
index 00000000000..a01c71432c3
--- /dev/null
+++ b/spec/services/ci/pipeline_schedules/create_service_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuous_integration do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
+ before_all do
+ project.add_maintainer(user)
+ project.add_reporter(reporter)
+ end
+
+ describe "execute" do
+ context 'when user does not have permission' do
+ subject(:service) { described_class.new(project, reporter, {}) }
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+
+ error_message = _('The current user is not authorized to create the pipeline schedule')
+ expect(result.message).to match_array([error_message])
+ expect(result.payload.errors).to match_array([error_message])
+ end
+ end
+
+ context 'when user has permission' do
+ let(:params) do
+ {
+ description: 'desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *',
+ cron_timezone: 'UTC'
+ }
+ end
+
+ subject(:service) { described_class.new(project, user, params) }
+
+ it 'saves values with passed params' do
+ result = service.execute
+
+ expect(result.payload).to have_attributes(
+ description: 'desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *',
+ cron_timezone: 'UTC'
+ )
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ end
+
+ context 'when schedule save fails' do
+ subject(:service) { described_class.new(project, user, {}) }
+
+ before do
+ errors = ActiveModel::Errors.new(project)
+ errors.add(:base, 'An error occurred')
+
+ allow_next_instance_of(Ci::PipelineSchedule) do |instance|
+ allow(instance).to receive(:save).and_return(false)
+ allow(instance).to receive(:errors).and_return(errors)
+ end
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to match_array(['An error occurred'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb
index 838f49f6dea..c31a652ed93 100644
--- a/spec/services/ci/pipeline_schedules/update_service_spec.rb
+++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb
@@ -8,9 +8,16 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+ let_it_be(:pipeline_schedule_variable) do
+ create(:ci_pipeline_schedule_variable,
+ key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
+ end
+
before_all do
project.add_maintainer(user)
project.add_reporter(reporter)
+
+ pipeline_schedule.reload
end
describe "execute" do
@@ -22,7 +29,10 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
expect(result).to be_a(ServiceResponse)
expect(result.error?).to be(true)
- expect(result.message).to eq(_('The current user is not authorized to update the pipeline schedule'))
+
+ error_message = _('The current user is not authorized to update the pipeline schedule')
+ expect(result.message).to match_array([error_message])
+ expect(pipeline_schedule.errors).to match_array([error_message])
end
end
@@ -32,7 +42,10 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
description: 'updated_desc',
ref: 'patch-x',
active: false,
- cron: '*/1 * * * *'
+ cron: '*/1 * * * *',
+ variables_attributes: [
+ { id: pipeline_schedule_variable.id, key: 'bar', secret_value: 'barvalue' }
+ ]
}
end
@@ -44,6 +57,42 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
.and change { pipeline_schedule.ref }.from('master').to('patch-x')
.and change { pipeline_schedule.active }.from(true).to(false)
.and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
+ .and change { pipeline_schedule.variables.last.key }.from('foo').to('bar')
+ .and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue')
+ end
+
+ context 'when creating a variable' do
+ let(:params) do
+ {
+ variables_attributes: [
+ { key: 'ABC', secret_value: 'ABC123' }
+ ]
+ }
+ end
+
+ it 'creates the new variable' do
+ expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(1)
+
+ expect(pipeline_schedule.variables.last.key).to eq('ABC')
+ expect(pipeline_schedule.variables.last.value).to eq('ABC123')
+ end
+ end
+
+ context 'when deleting a variable' do
+ let(:params) do
+ {
+ variables_attributes: [
+ {
+ id: pipeline_schedule_variable.id,
+ _destroy: true
+ }
+ ]
+ }
+ end
+
+ it 'deletes the existing variable' do
+ expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(-1)
+ end
end
it 'returns ServiceResponse.success' do
@@ -58,7 +107,7 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
subject(:service) { described_class.new(pipeline_schedule, user, {}) }
before do
- allow(pipeline_schedule).to receive(:update).and_return(false)
+ allow(pipeline_schedule).to receive(:save).and_return(false)
errors = ActiveModel::Errors.new(pipeline_schedule)
errors.add(:base, 'An error occurred')
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index b6e07e82bb5..fbd1a765351 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe Ci::PipelineTriggerService, feature_category: :continuous_integra
shared_examples 'detecting an unprocessable pipeline trigger' do
context 'when the pipeline was not created successfully' do
let(:fail_pipeline) do
- receive(:execute).and_wrap_original do |original, *args|
- response = original.call(*args)
+ receive(:execute).and_wrap_original do |original, *args, **kwargs|
+ response = original.call(*args, **kwargs)
pipeline = response.payload
pipeline.update!(failure_reason: 'unknown_failure')
diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb
index 84b6d7d96f6..ff9bcd0f8e9 100644
--- a/spec/services/ci/process_sync_events_service_spec.rb
+++ b/spec/services/ci/process_sync_events_service_spec.rb
@@ -145,9 +145,9 @@ RSpec.describe Ci::ProcessSyncEventsService, feature_category: :continuous_integ
end
end
- context 'when the FFs use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do
+ context 'when the use_traversal_ids FF is disabled' do
before do
- stub_feature_flags(use_traversal_ids: false, use_traversal_ids_for_ancestors: false)
+ stub_feature_flags(use_traversal_ids: false)
end
it_behaves_like 'event consuming'
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 6fb61bb3ec5..61fec82c688 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -417,8 +417,8 @@ module Ci
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!)
+ allow_any_instance_of(described_class).to receive(:assign_runner!).and_call_original
+ allow_any_instance_of(described_class).to receive(:assign_runner!)
.with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
end
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index b5921773364..7252763c13e 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
expect(runner).to be_an_instance_of(::Ci::Runner)
expect(runner.persisted?).to be_falsey
expect(runner.errors.messages).to eq(
- 'runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded']
+ runner_projects: ['Maximum number of ci registered project runners (1) exceeded']
)
expect(project.runners.reload.size).to eq(1)
end
@@ -252,7 +252,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
expect(runner).to be_an_instance_of(::Ci::Runner)
expect(runner.persisted?).to be_falsey
expect(runner.errors.messages).to eq(
- 'runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded']
+ runner_namespaces: ['Maximum number of ci registered group runners (1) exceeded']
)
expect(group.runners.reload.size).to eq(1)
end
diff --git a/spec/services/clusters/agent_tokens/create_service_spec.rb b/spec/services/clusters/agent_tokens/create_service_spec.rb
index 431d7ce2079..fde39b1099e 100644
--- a/spec/services/clusters/agent_tokens/create_service_spec.rb
+++ b/spec/services/clusters/agent_tokens/create_service_spec.rb
@@ -89,21 +89,6 @@ RSpec.describe Clusters::AgentTokens::CreateService, feature_category: :deployme
expect(subject.status).to eq(:error)
expect(subject.message).to eq('An agent can have only two active tokens at a time')
end
-
- context 'when cluster_agents_limit_tokens_created feature flag is disabled' do
- before do
- stub_feature_flags(cluster_agents_limit_tokens_created: false)
- end
-
- it 'creates a new token' do
- expect { subject }.to change { ::Clusters::AgentToken.count }.by(1)
- end
-
- it 'returns success status', :aggregate_failures do
- expect(subject.status).to eq(:success)
- expect(subject.message).to be_nil
- end
- end
end
end
end
diff --git a/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb b/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb
index 2d6c79c5cb3..b1e28c903f4 100644
--- a/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb
+++ b/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb
@@ -31,6 +31,8 @@ RSpec.describe Clusters::Agents::AuthorizeProxyUserService, feature_category: :d
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
+ expect(service_response.message)
+ .to eq 'You must be a member of `projects` or `groups` under the `user_access` keyword.'
end
context 'when user is member of an authorized group' do
@@ -45,6 +47,8 @@ RSpec.describe Clusters::Agents::AuthorizeProxyUserService, feature_category: :d
deployment_group.add_member(user, :reporter)
expect(service_response).to be_error
expect(service_response.reason).to eq :forbidden
+ expect(service_response.message)
+ .to eq 'You must be a member of `projects` or `groups` under the `user_access` keyword.'
end
end
@@ -60,6 +64,18 @@ RSpec.describe Clusters::Agents::AuthorizeProxyUserService, feature_category: :d
deployment_project.add_member(user, :reporter)
expect(service_response).to be_error
expect(service_response.reason).to eq :forbidden
+ expect(service_response.message)
+ .to eq 'You must be a member of `projects` or `groups` under the `user_access` keyword.'
+ end
+ end
+
+ context 'when config is empty' do
+ let(:user_access_config) { {} }
+
+ it 'returns an error', :aggregate_failures do
+ expect(service_response).to be_error
+ expect(service_response.reason).to eq :forbidden
+ expect(service_response.message).to eq '`user_access` keyword is not found in agent config file.'
end
end
end
diff --git a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
deleted file mode 100644
index 9390d4b368b..00000000000
--- a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Integrations::PrometheusHealthCheckService, '#execute', feature_category: :deployment_management do
- let(:service) { described_class.new(cluster) }
-
- subject { service.execute }
-
- RSpec.shared_examples 'no alert' do
- it 'does not send alert' do
- expect(Projects::Alerting::NotifyService).not_to receive(:new)
-
- subject
- end
- end
-
- RSpec.shared_examples 'sends alert' do
- it 'sends an alert' do
- expect_next_instance_of(Projects::Alerting::NotifyService) do |notify_service|
- expect(notify_service).to receive(:execute).with(integration.token, integration)
- end
-
- subject
- end
- end
-
- RSpec.shared_examples 'correct health stored' do
- it 'stores the correct health of prometheus' do
- subject
-
- expect(prometheus.healthy?).to eq(client_healthy)
- end
- end
-
- context 'when cluster is not project_type' do
- let(:cluster) { create(:cluster, :instance) }
-
- it { expect { subject }.to raise_error(RuntimeError, 'Invalid cluster type. Only project types are allowed.') }
- end
-
- context 'when cluster is project_type' do
- let_it_be(:project) { create(:project) }
- let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
-
- let(:previous_health_status) { :healthy }
- let(:prometheus) { create(:clusters_integrations_prometheus, enabled: prometheus_enabled, health_status: previous_health_status) }
- let(:cluster) { create(:cluster, :project, integration_prometheus: prometheus, projects: [project]) }
-
- context 'when prometheus not enabled' do
- let(:prometheus_enabled) { false }
-
- it { expect(subject).to eq(nil) }
-
- include_examples 'no alert'
- end
-
- context 'when prometheus enabled' do
- let(:prometheus_enabled) { true }
-
- before do
- client = instance_double('Gitlab::PrometheusClient', healthy?: client_healthy)
- expect(prometheus).to receive(:prometheus_client).and_return(client)
- end
-
- context 'when newly unhealthy' do
- let(:previous_health_status) { :healthy }
- let(:client_healthy) { false }
-
- include_examples 'sends alert'
- include_examples 'correct health stored'
- end
-
- context 'when newly healthy' do
- let(:previous_health_status) { :unhealthy }
- let(:client_healthy) { true }
-
- include_examples 'no alert'
- include_examples 'correct health stored'
- end
-
- context 'when continuously unhealthy' do
- let(:previous_health_status) { :unhealthy }
- let(:client_healthy) { false }
-
- include_examples 'no alert'
- include_examples 'correct health stored'
- end
-
- context 'when continuously healthy' do
- let(:previous_health_status) { :healthy }
- let(:client_healthy) { true }
-
- include_examples 'no alert'
- include_examples 'correct health stored'
- end
-
- context 'when first health check and healthy' do
- let(:previous_health_status) { :unknown }
- let(:client_healthy) { true }
-
- include_examples 'no alert'
- include_examples 'correct health stored'
- end
-
- context 'when first health check and not healthy' do
- let(:previous_health_status) { :unknown }
- let(:client_healthy) { false }
-
- include_examples 'sends alert'
- include_examples 'correct health stored'
- end
- end
- end
-end
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 08442f221fa..d02d472f897 100644
--- a/spec/services/design_management/generate_image_versions_service_spec.rb
+++ b/spec/services/design_management/generate_image_versions_service_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe DesignManagement::GenerateImageVersionsService, feature_category:
.from(nil).to(CarrierWave::SanitizedFile)
end
- it 'skips generating image versions if the mime type is not whitelisted' do
+ it 'skips generating image versions if the mime type is not allowlisted' do
stub_const('DesignManagement::DesignV432x230Uploader::MIME_TYPE_ALLOWLIST', [])
described_class.new(version).execute
diff --git a/spec/services/draft_notes/create_service_spec.rb b/spec/services/draft_notes/create_service_spec.rb
index 93731a80dcc..6288579025f 100644
--- a/spec/services/draft_notes/create_service_spec.rb
+++ b/spec/services/draft_notes/create_service_spec.rb
@@ -108,4 +108,18 @@ RSpec.describe DraftNotes::CreateService, feature_category: :code_review_workflo
end
end
end
+
+ context 'when the draft note is invalid' do
+ before do
+ allow_next_instance_of(DraftNote) do |draft|
+ allow(draft).to receive(:valid?).and_return(false)
+ end
+ end
+
+ it 'does not create the note' do
+ draft_note = create_draft(note: 'invalid note')
+
+ expect(draft_note).not_to be_persisted
+ end
+ end
end
diff --git a/spec/services/environments/create_service_spec.rb b/spec/services/environments/create_service_spec.rb
index d7fdfd2a38e..c7d32f9111a 100644
--- a/spec/services/environments/create_service_spec.rb
+++ b/spec/services/environments/create_service_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Environments::CreateService, feature_category: :environment_manag
describe '#execute' do
subject { service.execute }
- let(:params) { { name: 'production', external_url: 'https://gitlab.com', tier: :production } }
+ let(:params) { { name: 'production', external_url: 'https://gitlab.com', tier: :production, kubernetes_namespace: 'default' } }
it 'creates an environment' do
expect { subject }.to change { ::Environment.count }.by(1)
@@ -27,6 +27,7 @@ RSpec.describe Environments::CreateService, feature_category: :environment_manag
expect(response.payload[:environment].name).to eq('production')
expect(response.payload[:environment].external_url).to eq('https://gitlab.com')
expect(response.payload[:environment].tier).to eq('production')
+ expect(response.payload[:environment].kubernetes_namespace).to eq('default')
end
context 'with a cluster agent' do
diff --git a/spec/services/environments/update_service_spec.rb b/spec/services/environments/update_service_spec.rb
index 84220c0930b..808d6340314 100644
--- a/spec/services/environments/update_service_spec.rb
+++ b/spec/services/environments/update_service_spec.rb
@@ -28,6 +28,21 @@ RSpec.describe Environments::UpdateService, feature_category: :environment_manag
expect(response.payload[:environment]).to eq(environment)
end
+ context 'when setting a kubernetes namespace to the environment' do
+ let(:params) { { kubernetes_namespace: 'default' } }
+
+ it 'updates the kubernetes namespace' do
+ expect { subject }.to change { environment.reload.kubernetes_namespace }.to('default')
+ end
+
+ it 'returns successful response' do
+ response = subject
+
+ expect(response).to be_success
+ expect(response.payload[:environment]).to eq(environment)
+ end
+ end
+
context 'when setting a cluster agent to the environment' do
let_it_be(:agent_management_project) { create(:project) }
let_it_be(:cluster_agent) { create(:cluster_agent, project: agent_management_project) }
diff --git a/spec/services/error_tracking/issue_details_service_spec.rb b/spec/services/error_tracking/issue_details_service_spec.rb
index 7ac41ffead6..b1c963e2487 100644
--- a/spec/services/error_tracking/issue_details_service_spec.rb
+++ b/spec/services/error_tracking/issue_details_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
include_context 'sentry error tracking context'
- subject { described_class.new(project, user, params) }
+ subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
context 'with authorized user' do
@@ -41,26 +41,41 @@ RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_trac
include_examples 'error tracking service http status handling', :issue_details
context 'with integrated error tracking' do
- let_it_be(:error) { create(:error_tracking_error, project: project) }
-
- let(:params) { { issue_id: error.id } }
+ let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
+ let(:params) { { issue_id: issue_id } }
before do
error_tracking_setting.update!(integrated: true)
+
+ allow(service).to receive(:error_repository).and_return(error_repository)
end
- it 'returns the error in detailed format' do
- expect(result[:status]).to eq(:success)
- expect(result[:issue].to_json).to eq(error.to_sentry_detailed_error.to_json)
+ context 'when error is found' do
+ let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
+ let(:issue_id) { error.fingerprint }
+
+ before do
+ allow(error_repository).to receive(:find_error).with(issue_id).and_return(error)
+ end
+
+ it 'returns the error in detailed format' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:issue]).to eq(error)
+ end
end
context 'when error does not exist' do
- let(:params) { { issue_id: non_existing_record_id } }
+ let(:issue_id) { non_existing_record_id }
+
+ before do
+ allow(error_repository).to receive(:find_error).with(issue_id)
+ .and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
+ end
it 'returns the error in detailed format' do
expect(result).to match(
status: :error,
- message: /Couldn't find ErrorTracking::Error/,
+ message: /Error not found/,
http_status: :bad_request
)
end
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 bfde14c7ef1..7e0e8dd56a0 100644
--- a/spec/services/error_tracking/issue_latest_event_service_spec.rb
+++ b/spec/services/error_tracking/issue_latest_event_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
let(:params) { {} }
- subject { described_class.new(project, user, params) }
+ subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
context 'with authorized user' do
@@ -29,27 +29,42 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
include_examples 'error tracking service http status handling', :issue_latest_event
context 'with integrated error tracking' do
- let_it_be(:error) { create(:error_tracking_error, project: project) }
- let_it_be(:event) { create(:error_tracking_error_event, error: error) }
-
- let(:params) { { issue_id: error.id } }
+ let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
+ let(:params) { { issue_id: issue_id } }
before do
error_tracking_setting.update!(integrated: true)
+
+ allow(service).to receive(:error_repository).and_return(error_repository)
end
- it 'returns the latest event in expected format' do
- expect(result[:status]).to eq(:success)
- expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json)
+ context 'when error is found' do
+ let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
+ let(:event) { build_stubbed(:error_tracking_open_api_error_event, fingerprint: error.fingerprint) }
+ let(:issue_id) { error.fingerprint }
+
+ before do
+ allow(error_repository).to receive(:last_event_for).with(issue_id).and_return(event)
+ end
+
+ it 'returns the latest event in expected format' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:latest_event]).to eq(event)
+ end
end
context 'when error does not exist' do
- let(:params) { { issue_id: non_existing_record_id } }
+ let(:issue_id) { non_existing_record_id }
+
+ before do
+ allow(error_repository).to receive(:last_event_for).with(issue_id)
+ .and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
+ end
it 'returns the error in detailed format' do
expect(result).to match(
status: :error,
- message: /Couldn't find ErrorTracking::Error/,
+ message: /Error not found/,
http_status: :bad_request
)
end
diff --git a/spec/services/error_tracking/issue_update_service_spec.rb b/spec/services/error_tracking/issue_update_service_spec.rb
index 4dae6cc2fa0..989ebc86abe 100644
--- a/spec/services/error_tracking/issue_update_service_spec.rb
+++ b/spec/services/error_tracking/issue_update_service_spec.rb
@@ -113,17 +113,45 @@ RSpec.describe ErrorTracking::IssueUpdateService, feature_category: :error_track
include_examples 'error tracking service sentry error handling', :update_issue
context 'with integrated error tracking' do
- let(:error) { create(:error_tracking_error, project: project) }
- let(:arguments) { { issue_id: error.id, status: 'resolved' } }
- let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } }
+ let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
+ let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
+ let(:issue_id) { error.fingerprint }
+ let(:arguments) { { issue_id: issue_id, status: 'resolved' } }
before do
error_tracking_setting.update!(integrated: true)
+
+ allow(update_service).to receive(:error_repository).and_return(error_repository)
+ allow(error_repository).to receive(:update_error)
+ .with(issue_id, status: 'resolved').and_return(updated)
end
- it 'resolves the error and responds with expected format' do
- expect(update_service.execute).to eq(update_issue_response)
- expect(error.reload.status).to eq('resolved')
+ context 'when update succeeded' do
+ let(:updated) { true }
+
+ it 'returns success with updated true' do
+ expect(project.error_tracking_setting).to receive(:expire_issues_cache)
+
+ expect(update_service.execute).to eq(
+ status: :success,
+ updated: true,
+ closed_issue_iid: nil
+ )
+ end
+ end
+
+ context 'when update failed' do
+ let(:updated) { false }
+
+ it 'returns success with updated false' do
+ expect(project.error_tracking_setting).to receive(:expire_issues_cache)
+
+ expect(update_service.execute).to eq(
+ status: :success,
+ updated: false,
+ closed_issue_iid: nil
+ )
+ end
end
end
end
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index 2c35c2b8acd..5a6e9b56f6c 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -7,10 +7,10 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
let(:params) { {} }
- subject { described_class.new(project, user, params) }
+ subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
- context 'Sentry backend' do
+ context 'with Sentry backend' do
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
let(:list_sentry_issues_args) do
@@ -42,7 +42,7 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
expect(result).to eq(status: :success, pagination: {}, issues: issues)
end
- it 'returns bad request for an issue_status not on the whitelist' do
+ it 'returns bad request with invalid issue_status' do
params[:issue_status] = 'assigned'
expect(error_tracking_setting).not_to receive(:list_sentry_issues)
@@ -65,22 +65,84 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
end
end
- context 'GitLab backend' do
- let_it_be(:error1) { create(:error_tracking_error, name: 'foo', project: project) }
- let_it_be(:error2) { create(:error_tracking_error, name: 'bar', project: project) }
+ context 'with integrated error tracking' do
+ let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
+ let(:errors) { [] }
+ let(:pagination) { Gitlab::ErrorTracking::ErrorRepository::Pagination.new(nil, nil) }
+ let(:opts) { default_opts }
- let(:params) { { limit: '1' } }
+ let(:default_opts) do
+ {
+ filters: { status: described_class::DEFAULT_ISSUE_STATUS },
+ query: nil,
+ sort: described_class::DEFAULT_SORT,
+ limit: described_class::DEFAULT_LIMIT,
+ cursor: nil
+ }
+ end
+
+ let(:params) { {} }
before do
error_tracking_setting.update!(integrated: true)
+
+ allow(service).to receive(:error_repository).and_return(error_repository)
end
- it 'returns the error in expected format' do
- expect(result[:status]).to eq(:success)
- expect(result[:issues].size).to eq(1)
- expect(result[:issues].first.to_json).to eq(error2.to_sentry_error.to_json)
- expect(result[:pagination][:next][:cursor]).to be_present
- expect(result[:pagination][:previous]).to be_nil
+ context 'when errors are found' do
+ let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
+ let(:errors) { [error] }
+
+ before do
+ allow(error_repository).to receive(:list_errors)
+ .with(**opts)
+ .and_return([errors, pagination])
+ end
+
+ context 'without params' do
+ it 'returns the errors without pagination' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:issues]).to eq(errors)
+ expect(result[:pagination]).to eq({})
+ expect(error_repository).to have_received(:list_errors).with(**opts)
+ end
+ end
+
+ context 'with pagination' do
+ context 'with next page' do
+ before do
+ pagination.next = 'next cursor'
+ end
+
+ it 'has next cursor' do
+ expect(result[:pagination]).to eq(next: { cursor: 'next cursor' })
+ end
+ end
+
+ context 'with prev page' do
+ before do
+ pagination.prev = 'prev cursor'
+ end
+
+ it 'has prev cursor' do
+ expect(result[:pagination]).to eq(previous: { cursor: 'prev cursor' })
+ end
+ end
+
+ context 'with next and prev page' do
+ before do
+ pagination.next = 'next cursor'
+ pagination.prev = 'prev cursor'
+ end
+
+ it 'has both cursors' do
+ expect(result[:pagination]).to eq(
+ next: { cursor: 'next cursor' },
+ previous: { cursor: 'prev cursor' }
+ )
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index 8a686a19c4c..60883db0cd5 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -102,10 +102,25 @@ RSpec.describe Git::BaseHooksService, feature_category: :source_code_management
it 'executes the services' do
expect(subject).to receive(:push_data).at_least(:once).and_call_original
- expect(project).to receive(:execute_integrations)
+ expect(project).to receive(:execute_integrations).with(kind_of(Hash), subject.hook_name, skip_ci: false)
subject.execute
end
+
+ context 'with integrations.skip_ci push option' do
+ before do
+ params[:push_options] = {
+ integrations: { skip_ci: true }
+ }
+ end
+
+ it 'executes the services' do
+ expect(subject).to receive(:push_data).at_least(:once).and_call_original
+ expect(project).to receive(:execute_integrations).with(kind_of(Hash), subject.hook_name, skip_ci: true)
+
+ subject.execute
+ end
+ end
end
context 'with inactive integrations' do
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index f567624068a..3050d6c5eca 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -29,6 +29,7 @@ RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, featur
before: oldrev,
after: newrev,
ref: ref,
+ ref_protected: project.protected_for?(ref),
user_id: user.id,
user_name: user.name,
project_id: project.id
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
index 73f6eff36ba..3e06443126b 100644
--- a/spec/services/git/tag_hooks_service_spec.rb
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe Git::TagHooksService, :service, feature_category: :source_code_ma
is_expected.to match a_hash_including(
object_kind: 'tag_push',
ref: ref,
+ ref_protected: project.protected_for?(ref),
before: oldrev,
after: newrev,
message: tag.message,
diff --git a/spec/services/groups/participants_service_spec.rb b/spec/services/groups/participants_service_spec.rb
index eee9cfce1b1..0b370ca9fd8 100644
--- a/spec/services/groups/participants_service_spec.rb
+++ b/spec/services/groups/participants_service_spec.rb
@@ -3,25 +3,78 @@
require 'spec_helper'
RSpec.describe Groups::ParticipantsService, feature_category: :groups_and_projects do
- describe '#group_members' do
- let(:user) { create(:user) }
- let(:parent_group) { create(:group) }
- let(:group) { create(:group, parent: parent_group) }
- let(:subgroup) { create(:group, parent: group) }
- let(:subproject) { create(:project, group: subgroup) }
+ describe '#execute' do
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent_group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subproject) { create(:project, group: subgroup) }
+
+ let(:service) { described_class.new(group, developer) }
+
+ subject(:service_result) { service.execute(nil) }
+
+ before_all do
+ parent_group.add_developer(developer)
+ end
+
+ before do
+ stub_feature_flags(disable_all_mention: false)
+ end
+
+ it 'includes `All Group Members`' do
+ group.add_developer(create(:user))
+
+ # These should not be included in the count for the @all entry
+ subgroup.add_developer(create(:user))
+ subproject.add_developer(create(:user))
+
+ expect(service_result).to include(a_hash_including({ username: "all", name: "All Group Members", count: 1 }))
+ end
+
+ context 'when `disable_all_mention` FF is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+ end
+
+ it 'does not include `All Group Members`' do
+ expect(service_result).not_to include(a_hash_including({ username: "all", name: "All Group Members" }))
+ end
+ end
it 'returns all members in parent groups, sub-groups, and sub-projects' do
parent_group.add_developer(create(:user))
subgroup.add_developer(create(:user))
subproject.add_developer(create(:user))
- result = described_class.new(group, user).execute(nil)
-
expected_users = (group.self_and_hierarchy.flat_map(&:users) + subproject.users)
.map { |user| user_to_autocompletable(user) }
- expect(expected_users.count).to eq(3)
- expect(result).to include(*expected_users)
+ expect(expected_users.count).to eq(4)
+ expect(service_result).to include(*expected_users)
+ end
+
+ context 'when shared with a private group' do
+ let_it_be(:private_group_member) { create(:user) }
+ let_it_be(:private_group) { create(:group, :private, :nested) }
+
+ before_all do
+ private_group.add_owner(private_group_member)
+
+ create(:group_group_link, shared_group: parent_group, shared_with_group: private_group)
+ end
+
+ subject(:usernames) { service_result.pluck(:username) }
+
+ context 'when current_user is not a member' do
+ let(:service) { described_class.new(group, create(:user)) }
+
+ it { is_expected.not_to include(private_group_member.username) }
+ end
+
+ context 'when current_user is a member' do
+ it { is_expected.to include(private_group_member.username) }
+ end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 2842097199f..3819bcee36d 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -333,17 +333,27 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
described_class.new(internal_group, user, default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
+ let(:settings) { internal_group.namespace_settings }
+ let(:expected_settings) { Gitlab::Access::BranchProtection.protection_none.stringify_keys }
+
context 'for users who have the ability to update default_branch_protection' do
- it 'updates the attribute' do
+ it 'updates default_branch_protection attribute' do
+ internal_group.add_owner(user)
+
+ expect { service.execute }.to change { internal_group.default_branch_protection }.from(Gitlab::Access::PROTECTION_FULL).to(Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'updates default_branch_protection_defaults to match default_branch_protection' do
internal_group.add_owner(user)
- expect { service.execute }.to change { internal_group.default_branch_protection }.to(Gitlab::Access::PROTECTION_NONE)
+ expect { service.execute }.to change { settings.default_branch_protection_defaults }.from({}).to(expected_settings)
end
end
context 'for users who do not have the ability to update default_branch_protection' do
it 'does not update the attribute' do
expect { service.execute }.not_to change { internal_group.default_branch_protection }
+ expect { service.execute }.not_to change { internal_group.namespace_settings.default_branch_protection_defaults }
end
end
end
diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb
index 0acf1ec3d35..00eabb5c875 100644
--- a/spec/services/groups/update_shared_runners_service_spec.rb
+++ b/spec/services/groups/update_shared_runners_service_spec.rb
@@ -3,15 +3,17 @@
require 'spec_helper'
RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and_projects do
+ include ReloadHelpers
+
let(:user) { create(:user) }
- let(:group) { create(:group) }
let(:params) { {} }
+ let(:service) { described_class.new(group, user, params) }
describe '#execute' do
- subject { described_class.new(group, user, params).execute }
+ subject { service.execute }
context 'when current_user is not the group owner' do
- let_it_be(:group) { create(:group) }
+ let(:group) { create(:group) }
let(:params) { { shared_runners_setting: 'enabled' } }
@@ -19,9 +21,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
group.add_maintainer(user)
end
- it 'results error and does not call any method' do
- expect(group).not_to receive(:update_shared_runners_setting!)
-
+ it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Operation not allowed')
expect(subject[:http_status]).to eq(403)
@@ -36,23 +36,36 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
context 'enable shared Runners' do
let(:params) { { shared_runners_setting: 'enabled' } }
- context 'group that its ancestors have shared runners disabled' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled) }
- let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
+ context 'when ancestor disable shared runners' do
+ let(:parent) { create(:group, :shared_runners_disabled) }
+ let(:group) { create(:group, :shared_runners_disabled, parent: parent) }
+ let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
- it 'results error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq('Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
+ it 'returns an error and does not enable shared runners' do
+ expect do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
+
+ reload_models(parent, group, project)
+ end.to not_change { parent.shared_runners_enabled }
+ .and not_change { group.shared_runners_enabled }
+ .and not_change { project.shared_runners_enabled }
end
end
- context 'root group with shared runners disabled' do
- let_it_be(:group) { create(:group, :shared_runners_disabled) }
+ context 'when updating root group' do
+ let(:group) { create(:group, :shared_runners_disabled) }
+ let(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
+ let!(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
- it 'receives correct method and succeeds' do
- expect(group).to receive(:update_shared_runners_setting!).with('enabled')
+ it 'enables shared Runners for itself and descendants' do
+ expect do
+ expect(subject[:status]).to eq(:success)
- expect(subject[:status]).to eq(:success)
+ reload_models(group, sub_group, project)
+ end.to change { group.shared_runners_enabled }.from(false).to(true)
+ .and change { sub_group.shared_runners_enabled }.from(false).to(true)
+ .and change { project.shared_runners_enabled }.from(false).to(true)
end
end
@@ -75,7 +88,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
let(:params) { { shared_runners_setting: 'invalid_enabled' } }
it 'does not update pending builds for the group' do
- expect(::Ci::UpdatePendingBuildService).not_to receive(:new).and_call_original
+ expect(::Ci::UpdatePendingBuildService).not_to receive(:new)
subject
@@ -87,20 +100,46 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
end
context 'disable shared Runners' do
- let_it_be(:group) { create(:group) }
+ let!(:group) { create(:group) }
+ let!(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
+ let!(:sub_group2) { create(:group, parent: group) }
+ let!(:project) { create(:project, group: group, shared_runners_enabled: true) }
+ let!(:project2) { create(:project, group: sub_group2, shared_runners_enabled: true) }
let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE } }
- it 'receives correct method and succeeds' do
- expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_AND_UNOVERRIDABLE)
+ it 'disables shared Runners for all descendant groups and projects' do
+ expect do
+ expect(subject[:status]).to eq(:success)
+
+ reload_models(group, sub_group, sub_group2, project, project2)
+ end.to change { group.shared_runners_enabled }.from(true).to(false)
+ .and not_change { group.allow_descendants_override_disabled_shared_runners }
+ .and not_change { sub_group.shared_runners_enabled }
+ .and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
+ .and change { sub_group2.shared_runners_enabled }.from(true).to(false)
+ .and not_change { sub_group2.allow_descendants_override_disabled_shared_runners }
+ .and change { project.shared_runners_enabled }.from(true).to(false)
+ .and change { project2.shared_runners_enabled }.from(true).to(false)
+ end
+
+ context 'with override on self' do
+ let(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
+
+ it 'disables it' do
+ expect do
+ expect(subject[:status]).to eq(:success)
- expect(subject[:status]).to eq(:success)
+ group.reload
+ end
+ .to not_change { group.shared_runners_enabled }
+ .and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
+ end
end
context 'when group has pending builds' do
- let_it_be(:project) { create(:project, namespace: group) }
- let_it_be(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
- let_it_be(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
+ let!(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
+ let!(:pending_build_2) { create(:ci_pending_build, project: project, instance_runners_enabled: true) }
it 'updates pending builds for the group' do
expect(::Ci::UpdatePendingBuildService).to receive(:new).and_call_original
@@ -113,53 +152,91 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
end
end
- context 'allow descendants to override' do
- let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_OVERRIDABLE } }
-
+ shared_examples 'allow descendants to override' do
context 'top level group' do
- let_it_be(:group) { create(:group, :shared_runners_disabled) }
+ let!(:group) { create(:group, :shared_runners_disabled) }
+ let!(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
+ let!(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
- it 'receives correct method and succeeds' do
- expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_AND_OVERRIDABLE)
+ it 'enables allow descendants to override only for itself' do
+ expect do
+ expect(subject[:status]).to eq(:success)
- expect(subject[:status]).to eq(:success)
+ reload_models(group, sub_group, project)
+ end.to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
+ .and not_change { group.shared_runners_enabled }
+ .and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
+ .and not_change { sub_group.shared_runners_enabled }
+ .and not_change { project.shared_runners_enabled }
end
end
- context 'when parent does not allow' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
- let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
+ context 'when ancestor disables shared Runners but allows to override' do
+ let!(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
+ let!(:group) { create(:group, :shared_runners_disabled, parent: parent) }
+ let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
- it 'results error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
+ it 'enables allow descendants to override' do
+ expect do
+ expect(subject[:status]).to eq(:success)
+
+ reload_models(parent, group, project)
+ end
+ .to not_change { parent.allow_descendants_override_disabled_shared_runners }
+ .and not_change { parent.shared_runners_enabled }
+ .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
+ .and not_change { group.shared_runners_enabled }
+ .and not_change { project.shared_runners_enabled }
end
end
- context 'when using DISABLED_WITH_OVERRIDE (deprecated)' do
- let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } }
+ context 'when ancestor disables shared runners' do
+ let(:parent) { create(:group, :shared_runners_disabled) }
+ let(:group) { create(:group, :shared_runners_disabled, parent: parent) }
+ let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
- context 'top level group' do
- let_it_be(:group) { create(:group, :shared_runners_disabled) }
-
- it 'receives correct method and succeeds' do
- expect(group).to receive(:update_shared_runners_setting!).with(Namespace::SR_DISABLED_WITH_OVERRIDE)
+ it 'returns an error and does not enable shared runners' do
+ expect do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
- expect(subject[:status]).to eq(:success)
- end
+ reload_models(parent, group, project)
+ end.to not_change { parent.shared_runners_enabled }
+ .and not_change { group.shared_runners_enabled }
+ .and not_change { project.shared_runners_enabled }
end
+ end
- context 'when parent does not allow' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
- let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
+ context 'top level group that has shared Runners enabled' do
+ let!(:group) { create(:group, shared_runners_enabled: true) }
+ let!(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
+ let!(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
- it 'results error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq('Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
+ it 'enables allow descendants to override & disables shared runners everywhere' do
+ expect do
+ expect(subject[:status]).to eq(:success)
+
+ reload_models(group, sub_group, project)
end
+ .to change { group.shared_runners_enabled }.from(true).to(false)
+ .and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
+ .and change { sub_group.shared_runners_enabled }.from(true).to(false)
+ .and change { project.shared_runners_enabled }.from(true).to(false)
end
end
end
+
+ context "when using SR_DISABLED_AND_OVERRIDABLE" do
+ let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_AND_OVERRIDABLE } }
+
+ include_examples 'allow descendants to override'
+ end
+
+ context "when using SR_DISABLED_WITH_OVERRIDE" do
+ let(:params) { { shared_runners_setting: Namespace::SR_DISABLED_WITH_OVERRIDE } }
+
+ include_examples 'allow descendants to override'
+ end
end
end
end
diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb
index 21dc24e28f6..982b8b11383 100644
--- a/spec/services/import/github_service_spec.rb
+++ b/spec/services/import/github_service_spec.rb
@@ -5,7 +5,13 @@ require 'spec_helper'
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' } }
+ let_it_be(:access_params) do
+ {
+ github_access_token: 'github-complex-token',
+ additional_access_tokens: %w[foo bar]
+ }
+ end
+
let(:settings) { instance_double(Gitlab::GithubImport::Settings) }
let(:user_namespace_path) { user.namespace_path }
let(:optional_stages) { nil }
@@ -26,7 +32,12 @@ RSpec.describe Import::GithubService, feature_category: :importers do
before do
allow(Gitlab::GithubImport::Settings).to receive(:new).with(project_double).and_return(settings)
- allow(settings).to receive(:write).with(optional_stages)
+ allow(settings)
+ .to receive(:write)
+ .with(
+ optional_stages: optional_stages,
+ additional_access_tokens: access_params[:additional_access_tokens]
+ )
end
context 'do not raise an exception on input error' do
@@ -82,7 +93,9 @@ RSpec.describe Import::GithubService, feature_category: :importers do
context 'when there is no repository size limit defined' do
it 'skips the check, succeeds, and tracks an access level' do
expect(subject.execute(access_params, :github)).to include(status: :success)
- expect(settings).to have_received(:write).with(nil)
+ expect(settings)
+ .to have_received(:write)
+ .with(optional_stages: nil, additional_access_tokens: access_params[:additional_access_tokens])
expect_snowplow_event(
category: 'Import::GithubService',
action: 'create',
@@ -102,7 +115,9 @@ RSpec.describe Import::GithubService, feature_category: :importers do
it 'succeeds when the repository is smaller than the limit' do
expect(subject.execute(access_params, :github)).to include(status: :success)
- expect(settings).to have_received(:write).with(nil)
+ expect(settings)
+ .to have_received(:write)
+ .with(optional_stages: nil, additional_access_tokens: access_params[:additional_access_tokens])
expect_snowplow_event(
category: 'Import::GithubService',
action: 'create',
@@ -129,7 +144,9 @@ RSpec.describe Import::GithubService, feature_category: :importers do
context 'when application size limit is defined' do
it 'succeeds when the repository is smaller than the limit' do
expect(subject.execute(access_params, :github)).to include(status: :success)
- expect(settings).to have_received(:write).with(nil)
+ expect(settings)
+ .to have_received(:write)
+ .with(optional_stages: nil, additional_access_tokens: access_params[:additional_access_tokens])
expect_snowplow_event(
category: 'Import::GithubService',
action: 'create',
@@ -160,7 +177,22 @@ RSpec.describe Import::GithubService, feature_category: :importers do
it 'saves optional stages choice to import_data' do
subject.execute(access_params, :github)
- expect(settings).to have_received(:write).with(optional_stages)
+ expect(settings)
+ .to have_received(:write)
+ .with(
+ optional_stages: optional_stages,
+ additional_access_tokens: access_params[:additional_access_tokens]
+ )
+ end
+ end
+
+ context 'when additional access tokens are present' do
+ it 'saves additional access tokens to import_data' do
+ subject.execute(access_params, :github)
+
+ expect(settings)
+ .to have_received(:write)
+ .with(optional_stages: optional_stages, additional_access_tokens: %w[foo bar])
end
end
end
diff --git a/spec/services/import_csv/preprocess_milestones_service_spec.rb b/spec/services/import_csv/preprocess_milestones_service_spec.rb
new file mode 100644
index 00000000000..d21be52c9b9
--- /dev/null
+++ b/spec/services/import_csv/preprocess_milestones_service_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ImportCsv::PreprocessMilestonesService, feature_category: :importers do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:provided_titles) { %w[15.10 10.1] }
+
+ let(:service) { described_class.new(user, project, provided_titles) }
+
+ subject { service.execute }
+
+ describe '#execute' do
+ let(:project_milestones) { ::MilestonesFinder.new({ project_ids: [project.id] }).execute }
+
+ shared_examples 'csv import' do |is_success:, milestone_errors:|
+ it 'does not create milestones' do
+ expect { subject }.not_to change { project_milestones.count }
+ end
+
+ it 'reports any missing milestones' do
+ result = subject
+
+ if is_success
+ expect(result).to be_success
+ else
+ expect(result[:status]).to eq(:error)
+ expect(result.payload).to match(milestone_errors)
+ end
+ end
+ end
+
+ context 'with csv that has missing or unavailable milestones' do
+ it_behaves_like 'csv import',
+ { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 10.1] } } }
+ end
+
+ context 'with csv that includes project milestones' do
+ let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }
+
+ it_behaves_like 'csv import',
+ { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
+ end
+
+ context 'with csv that includes milestones column' do
+ let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }
+
+ context 'when milestones exist in the importing projects group' do
+ let!(:group_milestone) { create(:milestone, group: group, title: '10.1') }
+
+ it_behaves_like 'csv import', { is_success: true, milestone_errors: nil }
+ end
+
+ context 'when milestones exist in a subgroup of the importing projects group' do
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let!(:group_milestone) { create(:milestone, group: subgroup, title: '10.1') }
+
+ it_behaves_like 'csv import',
+ { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
+ end
+
+ context 'when milestones exist in a different project from the importing project' do
+ let_it_be(:second_project) { create(:project, group: group) }
+ let!(:second_project_milestone) { create(:milestone, project: second_project, title: '10.1') }
+
+ it_behaves_like 'csv import',
+ { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
+ end
+
+ context 'when duplicate milestones exist in the projects group and parent group' do
+ let_it_be(:sub_group) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, group: sub_group) }
+ let!(:ancestor_group_milestone) { create(:milestone, group: group, title: '15.10') }
+ let!(:ancestor_group_milestone_two) { create(:milestone, group: group, title: '10.1') }
+ let!(:group_milestone) { create(:milestone, group: sub_group, title: '10.1') }
+
+ it_behaves_like 'csv import', { is_success: true, milestone_errors: nil }
+ end
+ end
+ end
+end
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 9b1994af1bb..528921a80ee 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
@@ -9,7 +9,7 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateServic
let_it_be(:issue, reload: true) { escalation_status.issue }
let_it_be(:project) { issue.project }
- let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) }
+ let(:service) { described_class.new(issue, current_user) }
subject(:result) do
issue.update!(incident_management_issuable_escalation_status_attributes: update_params)
diff --git a/spec/services/integrations/group_mention_service_spec.rb b/spec/services/integrations/group_mention_service_spec.rb
new file mode 100644
index 00000000000..72d53ce6d06
--- /dev/null
+++ b/spec/services/integrations/group_mention_service_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GroupMentionService, feature_category: :integrations do
+ subject(:execute) { described_class.new(mentionable, hook_data: hook_data, is_confidential: is_confidential).execute }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before do
+ allow(mentionable).to receive(:referenced_groups).with(user).and_return([group])
+ end
+
+ shared_examples 'group_mention_hooks' do
+ specify do
+ expect(group).to receive(:execute_integrations).with(anything, :group_mention_hooks)
+ expect(execute).to be_success
+ end
+ end
+
+ shared_examples 'group_confidential_mention_hooks' do
+ specify do
+ expect(group).to receive(:execute_integrations).with(anything, :group_confidential_mention_hooks)
+ expect(execute).to be_success
+ end
+ end
+
+ context 'for issue descriptions' do
+ let(:hook_data) { mentionable.to_hook_data(user) }
+ let(:is_confidential) { mentionable.confidential? }
+
+ context 'in public projects' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'in public issues' do
+ let(:mentionable) do
+ create(:issue, confidential: false, project: project, author: user, description: "@#{group.full_path}")
+ end
+
+ it_behaves_like 'group_mention_hooks'
+ end
+
+ context 'in confidential issues' do
+ let(:mentionable) do
+ create(:issue, confidential: true, project: project, author: user, description: "@#{group.full_path}")
+ end
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+ end
+
+ context 'in private projects' do
+ let_it_be(:project) { create(:project, :private) }
+
+ context 'in public issues' do
+ let(:mentionable) do
+ create(:issue, confidential: false, project: project, author: user, description: "@#{group.full_path}")
+ end
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+
+ context 'in confidential issues' do
+ let(:mentionable) do
+ create(:issue, confidential: true, project: project, author: user, description: "@#{group.full_path}")
+ end
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+ end
+ end
+
+ context 'for merge request descriptions' do
+ let(:hook_data) { mentionable.to_hook_data(user) }
+ let(:is_confidential) { false }
+ let(:mentionable) do
+ create(:merge_request, source_project: project, target_project: project, author: user,
+ description: "@#{group.full_path}")
+ end
+
+ context 'in public projects' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it_behaves_like 'group_mention_hooks'
+ end
+
+ context 'in private projects' do
+ let_it_be(:project) { create(:project, :private) }
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+ end
+
+ context 'for issue notes' do
+ let(:hook_data) { Gitlab::DataBuilder::Note.build(mentionable, mentionable.author) }
+ let(:is_confidential) { mentionable.confidential?(include_noteable: true) }
+
+ context 'in public projects' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'in public issues' do
+ let(:issue) do
+ create(:issue, confidential: false, project: project, author: user, description: "@#{group.full_path}")
+ end
+
+ context 'for public notes' do
+ let(:mentionable) { create(:note_on_issue, noteable: issue, project: project, author: user) }
+
+ it_behaves_like 'group_mention_hooks'
+ end
+
+ context 'for internal notes' do
+ let(:mentionable) { create(:note_on_issue, :confidential, noteable: issue, project: project, author: user) }
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+ end
+ end
+
+ context 'in private projects' do
+ let_it_be(:project) { create(:project, :private) }
+
+ context 'in public issues' do
+ let(:issue) do
+ create(:issue, confidential: false, project: project, author: user, description: "@#{group.full_path}")
+ end
+
+ context 'for public notes' do
+ let(:mentionable) { create(:note_on_issue, noteable: issue, project: project, author: user) }
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+
+ context 'for internal notes' do
+ let(:mentionable) { create(:note_on_issue, :confidential, noteable: issue, project: project, author: user) }
+
+ it_behaves_like 'group_confidential_mention_hooks'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index 03b6a1b4556..446cc286e28 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Issuable::DiscussionsListService, feature_category: :team_plannin
it_behaves_like 'listing issuable discussions', :guest, 1, 7
context 'without notes widget' do
- let_it_be(:issuable) { create(:work_item, :issue, project: project) }
+ let_it_be(:issuable) { create(:work_item, project: project) }
before do
WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
diff --git a/spec/services/issuable/import_csv/base_service_spec.rb b/spec/services/issuable/import_csv/base_service_spec.rb
new file mode 100644
index 00000000000..b07c4556694
--- /dev/null
+++ b/spec/services/issuable/import_csv/base_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issuable::ImportCsv::BaseService, 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(:csv_io) { double }
+
+ let(:importer_klass) do
+ Class.new(described_class) do
+ def email_results_to_user
+ # no-op
+ end
+ end
+ end
+
+ let(:service) do
+ uploader = FileUploader.new(project)
+ uploader.store!(file)
+
+ importer_klass.new(user, project, uploader)
+ end
+
+ subject { service.execute }
+
+ describe '#preprocess_milestones' do
+ let(:utility_class) { ImportCsv::PreprocessMilestonesService }
+ let(:file) { fixture_file_upload('spec/fixtures/csv_missing_milestones.csv') }
+ let(:mocked_object) { double }
+
+ before do
+ allow(service).to receive(:create_object).and_return(mocked_object)
+ allow(mocked_object).to receive(:persisted?).and_return(true)
+ end
+
+ context 'with csv that has milestone heading' do
+ before do
+ allow(utility_class).to receive(:new).and_return(utility_class)
+ allow(utility_class).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ it 'calls PreprocessMilestonesService' do
+ subject
+ expect(utility_class).to have_received(:new)
+ end
+
+ it 'calls PreprocessMilestonesService with unique milestone titles' do
+ subject
+ expect(utility_class).to have_received(:new).with(user, project, %w[15.10 10.1])
+ expect(utility_class).to have_received(:execute)
+ end
+ end
+
+ context 'with csv that does not have milestone heading' do
+ let(:file) { fixture_file_upload('spec/fixtures/work_items_valid_types.csv') }
+
+ before do
+ allow(utility_class).to receive(:new).and_return(utility_class)
+ end
+
+ it 'does not call PreprocessMilestonesService' do
+ subject
+ expect(utility_class).not_to have_received(:new)
+ end
+ end
+
+ context 'when one or more milestones do not exist' do
+ it 'returns the expected error in results payload' do
+ results = subject
+
+ expect(results[:success]).to eq(0)
+ expect(results[:preprocess_errors]).to match({
+ milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 10.1] } }
+ })
+ end
+ end
+
+ context 'when all milestones exist' do
+ let!(:group_milestone) { create(:milestone, group: group, title: '10.1') }
+ let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }
+
+ it 'returns a successful response' do
+ results = subject
+
+ expect(results[:preprocess_errors]).to be_empty
+ expect(results[:success]).to eq(4)
+ end
+ end
+ end
+end
diff --git a/spec/services/issuable/process_assignees_spec.rb b/spec/services/issuable/process_assignees_spec.rb
index 2c8d4c5e11d..2751267c08b 100644
--- a/spec/services/issuable/process_assignees_spec.rb
+++ b/spec/services/issuable/process_assignees_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
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),
+ process = described_class.new(assignee_ids: %w(5 7 9),
add_assignee_ids: nil,
remove_assignee_ids: nil,
existing_assignee_ids: %w(1 3 9),
@@ -16,7 +16,7 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
end
it 'combines other ids when assignee_ids is nil' do
- process = Issuable::ProcessAssignees.new(assignee_ids: nil,
+ process = described_class.new(assignee_ids: nil,
add_assignee_ids: nil,
remove_assignee_ids: nil,
existing_assignee_ids: %w(1 3 11),
@@ -27,7 +27,7 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
end
it 'combines other ids when both add_assignee_ids and remove_assignee_ids are not empty' do
- process = Issuable::ProcessAssignees.new(assignee_ids: %w(5 7 9),
+ process = described_class.new(assignee_ids: %w(5 7 9),
add_assignee_ids: %w(2 4 6),
remove_assignee_ids: %w(4 7 11),
existing_assignee_ids: %w(1 3 11),
@@ -38,7 +38,7 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
end
it 'combines other ids when remove_assignee_ids is not empty' do
- process = Issuable::ProcessAssignees.new(assignee_ids: %w(5 7 9),
+ process = described_class.new(assignee_ids: %w(5 7 9),
add_assignee_ids: nil,
remove_assignee_ids: %w(4 7 11),
existing_assignee_ids: %w(1 3 11),
@@ -49,7 +49,7 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
end
it 'combines other ids when add_assignee_ids is not empty' do
- process = Issuable::ProcessAssignees.new(assignee_ids: %w(5 7 9),
+ process = described_class.new(assignee_ids: %w(5 7 9),
add_assignee_ids: %w(2 4 6),
remove_assignee_ids: nil,
existing_assignee_ids: %w(1 3 11),
@@ -60,7 +60,7 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
end
it 'combines ids when existing_assignee_ids and extra_assignee_ids are omitted' do
- process = Issuable::ProcessAssignees.new(assignee_ids: %w(5 7 9),
+ process = described_class.new(assignee_ids: %w(5 7 9),
add_assignee_ids: %w(2 4 6),
remove_assignee_ids: %w(4 7 11))
result = process.execute
@@ -69,7 +69,7 @@ RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
end
it 'handles mixed string and integer arrays' do
- process = Issuable::ProcessAssignees.new(assignee_ids: %w(5 7 9),
+ process = described_class.new(assignee_ids: %w(5 7 9),
add_assignee_ids: [2, 4, 6],
remove_assignee_ids: %w(4 7 11),
existing_assignee_ids: [1, 3, 11],
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 8368a34caf0..c51371ca0f2 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -205,7 +205,6 @@ RSpec.describe Issues::BuildService, feature_category: :team_planning do
issue = build_issue(**issue_params)
expect(issue.work_item_type_id).to eq(work_item_type_id)
- expect(issue.attributes['issue_type']).to eq(resulting_issue_type)
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 3dfc9571c9c..2daba8e359d 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -149,6 +149,12 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
issue
end
+ it 'calls GroupMentionWorker' do
+ expect(Integrations::GroupMentionWorker).to receive(:perform_async)
+
+ issue
+ end
+
context 'when a build_service is provided' do
let(:result) { described_class.new(container: project, current_user: user, params: opts, build_service: build_service).execute }
@@ -162,6 +168,29 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
end
end
+ context 'when issue template is provided' do
+ let_it_be(:files) { { '.gitlab/issue_templates/Default.md' => 'Default template contents' } }
+ let_it_be_with_reload(:project) { create(:project, :custom_repo, group: group, files: files).tap { |project| project.add_guest(user) } }
+
+ context 'when description is blank' do
+ it 'sets template contents as description when description is blank' do
+ opts[:description] = ''
+
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue).to have_attributes(description: 'Default template contents')
+ end
+ end
+
+ context 'when description is not blank' do
+ it 'does not apply template when description is not blank' do
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue).to have_attributes(description: 'please fix')
+ end
+ end
+ end
+
context 'when skip_system_notes is true' do
let(:issue) { described_class.new(container: project, current_user: user, params: opts).execute(skip_system_notes: true) }
diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb
index 68f1af49b5f..512990b5b3d 100644
--- a/spec/services/issues/relative_position_rebalancing_service_spec.rb
+++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb
@@ -156,5 +156,98 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
subject.execute
end
end
+
+ shared_examples 'no-op on the retried job' do
+ it 'does not update positions in the 2nd .execute' do
+ original_order = issues_in_position_order.map(&:id)
+
+ # preloads issue ids on both runs
+ expect(service).to receive(:preload_issue_ids).twice.and_call_original
+
+ # 1st run performs rebalancing
+ expect(service).to receive(:update_positions_with_retry).exactly(9).times.and_call_original
+ expect { service.execute }.to raise_error(StandardError)
+
+ # 2nd run is a no-op
+ expect(service).not_to receive(:update_positions_with_retry)
+ expect { service.execute }.to raise_error(StandardError)
+
+ # order is preserved
+ expect(original_order).to match_array(issues_in_position_order.map(&:id))
+ end
+ end
+
+ context 'when error is raised in cache cleanup step' do
+ let_it_be(:root_namespace_id) { project.root_namespace.id }
+
+ context 'when srem fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ allow(redis).to receive(:srem?).and_raise(StandardError)
+ end
+ end
+
+ it_behaves_like 'no-op on the retried job'
+ end
+
+ context 'when delete issues ids sorted set fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ allow(redis).to receive(:del).and_call_original
+ allow(redis).to receive(:del)
+ .with("#{Gitlab::Issues::Rebalancing::State::REDIS_KEY_PREFIX}:#{root_namespace_id}")
+ .and_raise(StandardError)
+ end
+ end
+
+ it_behaves_like 'no-op on the retried job'
+ end
+
+ context 'when delete current_index_key fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ allow(redis).to receive(:del).and_call_original
+ allow(redis).to receive(:del)
+ .with("#{Gitlab::Issues::Rebalancing::State::REDIS_KEY_PREFIX}:#{root_namespace_id}:current_index")
+ .and_raise(StandardError)
+ end
+ end
+
+ it_behaves_like 'no-op on the retried job'
+ end
+
+ context 'when setting recently finished key fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ allow(redis).to receive(:set).and_call_original
+ allow(redis).to receive(:set)
+ .with(
+ "#{Gitlab::Issues::Rebalancing::State::RECENTLY_FINISHED_REBALANCE_PREFIX}:2:#{project.id}",
+ anything,
+ anything
+ )
+ .and_raise(StandardError)
+ end
+ end
+
+ it 'reruns the next job in full' do
+ original_order = issues_in_position_order.map(&:id)
+
+ # preloads issue ids on both runs
+ expect(service).to receive(:preload_issue_ids).twice.and_call_original
+
+ # 1st run performs rebalancing
+ expect(service).to receive(:update_positions_with_retry).exactly(9).times.and_call_original
+ expect { service.execute }.to raise_error(StandardError)
+
+ # 2nd run performs rebalancing in full
+ expect(service).to receive(:update_positions_with_retry).exactly(9).times.and_call_original
+ expect { service.execute }.to raise_error(StandardError)
+
+ # order is preserved
+ expect(original_order).to match_array(issues_in_position_order.map(&:id))
+ end
+ end
+ end
end
end
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index bb1151dfac7..377efdb3f9f 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -68,6 +68,12 @@ RSpec.describe Issues::ReopenService, feature_category: :team_planning do
expect { execute }.not_to change { issue.incident_management_timeline_events.count }
end
+ it 'does not call GroupMentionWorker' do
+ expect(Integrations::GroupMentionWorker).not_to receive(:perform_async)
+
+ issue
+ end
+
context 'issue is incident type' do
let(:issue) { create(:incident, :closed, project: project) }
let(:current_user) { user }
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 1c0466980f4..76cd5d6c89e 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -219,6 +219,18 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
expect(result[:message][params[:email]]).to eq("Invite email is invalid")
end
end
+
+ context 'with email that has trailing spaces' do
+ let(:params) { { email: ' foo@bar.com ' } }
+
+ it 'returns an error' do
+ expect_not_to_create_members
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][params[:email]]).to eq("Invite email is invalid")
+ end
+
+ it_behaves_like 'does not record an onboarding progress action'
+ end
end
context 'with duplicate invites' do
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index 7255d19ef8a..1126539b25a 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -75,6 +75,12 @@ RSpec.describe MergeRequests::AfterCreateService, feature_category: :code_review
execute_service
end
+ it 'calls GroupMentionWorker' do
+ expect(Integrations::GroupMentionWorker).to receive(:perform_async)
+
+ execute_service
+ end
+
it_behaves_like 'records an onboarding progress action', :merge_request_created do
let(:namespace) { merge_request.target_project.namespace }
end
diff --git a/spec/services/merge_requests/cleanup_refs_service_spec.rb b/spec/services/merge_requests/cleanup_refs_service_spec.rb
index 960b8101c36..efb6265e3d8 100644
--- a/spec/services/merge_requests/cleanup_refs_service_spec.rb
+++ b/spec/services/merge_requests/cleanup_refs_service_spec.rb
@@ -18,6 +18,8 @@ RSpec.describe MergeRequests::CleanupRefsService, feature_category: :code_review
describe '#execute' do
before do
+ stub_feature_flags(merge_request_cleanup_ref_worker_async: false)
+
# Need to re-enable this as it's being stubbed in spec_helper for
# performance reasons but is needed to run for this test.
allow(Gitlab::Git::KeepAround).to receive(:execute).and_call_original
diff --git a/spec/services/merge_requests/merge_orchestration_service_spec.rb b/spec/services/merge_requests/merge_orchestration_service_spec.rb
index 389956bf258..b9bf936eddd 100644
--- a/spec/services/merge_requests/merge_orchestration_service_spec.rb
+++ b/spec/services/merge_requests/merge_orchestration_service_spec.rb
@@ -45,7 +45,17 @@ RSpec.describe MergeRequests::MergeOrchestrationService, feature_category: :code
subject
expect(merge_request).to be_auto_merge_enabled
- expect(merge_request.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+
+ if Gitlab.ee?
+ expect(merge_request.auto_merge_strategy).to(
+ eq(AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS)
+ )
+ else
+ expect(merge_request.auto_merge_strategy).to(
+ eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ )
+ end
+
expect(merge_request).not_to be_merged
end
end
@@ -108,7 +118,11 @@ RSpec.describe MergeRequests::MergeOrchestrationService, feature_category: :code
merge_request.update_head_pipeline
end
- it 'fetches perferred auto merge strategy' do
+ it 'fetches preferred auto merge strategy', if: Gitlab.ee? do
+ is_expected.to eq(AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS)
+ end
+
+ it 'fetches preferred auto merge strategy', unless: Gitlab.ee? do
is_expected.to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
end
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 8200f60b072..4e951f1bc85 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -288,17 +288,5 @@ RSpec.describe MergeRequests::MergeToRefService, feature_category: :code_review_
end
end
end
-
- context 'allow conflicts to be merged in diff' do
- let(:params) { { allow_conflicts: true } }
-
- it 'calls merge_to_ref with allow_conflicts param' do
- expect(project.repository).to receive(:merge_to_ref) do |user, **kwargs|
- expect(kwargs[:allow_conflicts]).to eq(true)
- end.and_call_original
-
- service.execute(merge_request)
- end
- end
end
end
diff --git a/spec/services/merge_requests/mergeability_check_batch_service_spec.rb b/spec/services/merge_requests/mergeability_check_batch_service_spec.rb
new file mode 100644
index 00000000000..099b8039f3e
--- /dev/null
+++ b/spec/services/merge_requests/mergeability_check_batch_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::MergeabilityCheckBatchService, feature_category: :code_review_workflow do
+ describe '#execute' do
+ subject { described_class.new(merge_requests, user).execute }
+
+ let(:merge_requests) { [] }
+ let_it_be(:user) { create(:user) }
+
+ context 'when merge_requests are not empty' do
+ let_it_be(:merge_request_1) { create(:merge_request) }
+ let_it_be(:merge_request_2) { create(:merge_request) }
+ let_it_be(:merge_requests) { [merge_request_1, merge_request_2] }
+
+ it 'triggers batch mergeability checks' do
+ expect(MergeRequests::MergeabilityCheckBatchWorker).to receive(:perform_async)
+ .with([merge_request_1.id, merge_request_2.id], user.id)
+
+ subject
+ end
+
+ context 'when user is nil' do
+ let(:user) { nil }
+
+ it 'trigger mergeability checks with nil user_id' do
+ expect(MergeRequests::MergeabilityCheckBatchWorker).to receive(:perform_async)
+ .with([merge_request_1.id, merge_request_2.id], nil)
+
+ subject
+ end
+ end
+ end
+
+ context 'when merge_requests is empty' do
+ let(:merge_requests) { MergeRequest.none }
+
+ it 'does not trigger mergeability checks' do
+ expect(MergeRequests::MergeabilityCheckBatchWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 4d533b67690..06932af26dc 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1024,4 +1024,49 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
end
end
end
+
+ describe '#abort_auto_merges' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author) { user }
+
+ let_it_be(:merge_request, refind: true) do
+ create(
+ :merge_request,
+ source_project: project,
+ target_project: project,
+ merge_user: user,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
+ )
+ end
+
+ let(:service) { described_class.new(project: project, current_user: user) }
+ let(:oldrev) { merge_request.diff_refs.base_sha }
+ let(:newrev) { merge_request.diff_refs.head_sha }
+ let(:merge_sha) { oldrev }
+
+ before do
+ merge_request.merge_params[:sha] = merge_sha
+ merge_request.save!
+
+ service.execute(oldrev, newrev, "refs/heads/#{merge_request.source_branch}")
+
+ merge_request.reload
+ end
+
+ it 'aborts MWPS for merge requests' do
+ expect(merge_request.auto_merge_enabled?).to be_falsey
+ expect(merge_request.merge_user).to be_nil
+ end
+
+ context 'when merge params contains up-to-date sha' do
+ let(:merge_sha) { newrev }
+
+ it 'maintains MWPS for merge requests' do
+ expect(merge_request.auto_merge_enabled?).to be_truthy
+ expect(merge_request.merge_user).to eq(user)
+ end
+ end
+ end
end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 7399b29d06e..e173cd382f2 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -40,6 +40,10 @@ RSpec.describe MergeRequests::ReopenService, feature_category: :code_review_work
.with(merge_request, 'reopen')
end
+ it 'does not call GroupMentionWorker' do
+ expect(Integrations::GroupMentionWorker).not_to receive(:perform_async)
+ end
+
it 'sends email to user2 about reopen of merge_request', :sidekiq_might_not_need_inline do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 52999b5a1ea..79f608a4614 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_description_edit_action).once.with(user: user)
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request2)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request2)
end
it 'tracks Draft marking' do
@@ -117,7 +117,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:title] = "Draft: #{opts[:title]}"
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request2)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request2)
end
it 'tracks Draft un-marking' do
@@ -126,7 +126,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:title] = "Non-draft/wip title string"
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(draft_merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(draft_merge_request)
end
context 'when MR is locked' do
@@ -137,7 +137,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:discussion_locked] = true
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
end
@@ -148,7 +148,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:discussion_locked] = false
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
end
end
@@ -163,7 +163,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:discussion_locked] = false
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
end
@@ -174,7 +174,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:discussion_locked] = true
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
end
end
@@ -193,7 +193,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
spent_at: Date.parse('2021-02-24')
}
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
it 'tracks milestone change' do
@@ -202,7 +202,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:milestone_id] = milestone.id
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
it 'track labels change' do
@@ -211,7 +211,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:label_ids] = [label2.id]
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
context 'reviewers' do
@@ -222,7 +222,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:reviewers] = [user2]
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
end
@@ -233,7 +233,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
opts[:reviewers] = merge_request.reviewers
- MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
+ described_class.new(project: project, current_user: user, params: opts).execute(merge_request)
end
end
end
@@ -449,7 +449,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
let(:milestone) { create(:milestone, project: project) }
let(:req_opts) { { source_branch: 'feature', target_branch: 'master' } }
- subject { MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) }
+ subject { described_class.new(project: project, current_user: user, params: opts).execute(merge_request) }
context 'when mentionable attributes change' do
let(:opts) { { description: "Description with #{user.to_reference}" }.merge(req_opts) }
@@ -552,7 +552,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
head_pipeline_of: merge_request
)
- expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, { sha: merge_request.diff_head_sha })
+ strategies_count = Gitlab.ee? ? :twice : :once
+ expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).exactly(strategies_count).with(project, user, { sha: merge_request.diff_head_sha })
.and_return(service_mock)
allow(service_mock).to receive(:available_for?) { true }
expect(service_mock).to receive(:execute).with(merge_request)
diff --git a/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb b/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb
index beed23a366f..53def716de3 100644
--- a/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Metrics::Dashboard::ClusterDashboardService, :use_clean_rails_mem
end
describe '#get_dashboard' do
- let(:service_params) { [project, user, { cluster: cluster, cluster_type: :project }] }
+ let(:service_params) { [project, user, { cluster: cluster, cluster_type: :admin }] }
let(:service_call) { subject.get_dashboard }
subject { described_class.new(*service_params) }
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index 78cb05532eb..70010d88fbd 100644
--- a/spec/services/milestones/create_service_spec.rb
+++ b/spec/services/milestones/create_service_spec.rb
@@ -3,24 +3,70 @@
require 'spec_helper'
RSpec.describe Milestones::CreateService, feature_category: :team_planning do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:params) { { title: 'New Milestone', description: 'Description' } }
+
+ subject(:create_milestone) { described_class.new(project, user, params) }
describe '#execute' do
- context "valid params" do
+ context 'when milestone is saved successfully' do
+ it 'creates a new milestone' do
+ expect { create_milestone.execute }.to change { Milestone.count }.by(1)
+ end
+
+ it 'opens the milestone if it is a project milestone' do
+ expect_next_instance_of(EventCreateService) do |instance|
+ expect(instance).to receive(:open_milestone)
+ end
+
+ create_milestone.execute
+ end
+
+ it 'returns the created milestone' do
+ milestone = create_milestone.execute
+ expect(milestone).to be_a(Milestone)
+ expect(milestone.title).to eq('New Milestone')
+ expect(milestone.description).to eq('Description')
+ end
+ end
+
+ context 'when milestone fails to save' do
before do
- project.add_maintainer(user)
+ allow_next_instance_of(Milestone) do |instance|
+ allow(instance).to receive(:save).and_return(false)
+ end
+ end
+
+ it 'does not create a new milestone' do
+ expect { create_milestone.execute }.not_to change { Milestone.count }
+ end
- opts = {
- title: 'v2.1.9',
- description: 'Patch release to fix security issue'
- }
+ it 'does not open the milestone' do
+ expect(EventCreateService).not_to receive(:open_milestone)
+
+ create_milestone.execute
+ end
- @milestone = described_class.new(project, user, opts).execute
+ it 'returns the unsaved milestone' do
+ milestone = create_milestone.execute
+ expect(milestone).to be_a(Milestone)
+ expect(milestone.title).to eq('New Milestone')
+ expect(milestone.persisted?).to be_falsey
end
+ end
+
+ it 'calls before_create method' do
+ expect(create_milestone).to receive(:before_create)
+ create_milestone.execute
+ end
+ end
- it { expect(@milestone).to be_valid }
- it { expect(@milestone.title).to eq('v2.1.9') }
+ describe '#before_create' do
+ it 'checks for spam' do
+ milestone = build(:milestone)
+ expect(milestone).to receive(:check_for_spam).with(user: user, action: :create)
+ subject.send(:before_create, milestone)
end
end
end
diff --git a/spec/services/milestones/update_service_spec.rb b/spec/services/milestones/update_service_spec.rb
index 76110af2514..44de49960d4 100644
--- a/spec/services/milestones/update_service_spec.rb
+++ b/spec/services/milestones/update_service_spec.rb
@@ -2,40 +2,86 @@
require 'spec_helper'
RSpec.describe Milestones::UpdateService, feature_category: :team_planning do
- let(:project) { create(:project) }
- let(:user) { build(:user) }
- let(:milestone) { create(:milestone, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:params) { { title: 'New Title' } }
+
+ subject(:update_milestone) { described_class.new(project, user, params) }
describe '#execute' do
- context "valid params" do
- let(:inner_service) { double(:service) }
+ context 'when state_event is "activate"' do
+ let(:params) { { state_event: 'activate' } }
- before do
- project.add_maintainer(user)
+ it 'calls Milestones::ReopenService' do
+ reopen_service = instance_double(Milestones::ReopenService)
+ expect(Milestones::ReopenService).to receive(:new).with(project, user, {}).and_return(reopen_service)
+ expect(reopen_service).to receive(:execute).with(milestone)
+
+ update_milestone.execute(milestone)
end
+ end
- subject { described_class.new(project, user, { title: 'new_title' }).execute(milestone) }
+ context 'when state_event is "close"' do
+ let(:params) { { state_event: 'close' } }
+
+ it 'calls Milestones::CloseService' do
+ close_service = instance_double(Milestones::CloseService)
+ expect(Milestones::CloseService).to receive(:new).with(project, user, {}).and_return(close_service)
+ expect(close_service).to receive(:execute).with(milestone)
+
+ update_milestone.execute(milestone)
+ end
+ end
- it { expect(subject).to be_valid }
- it { expect(subject.title).to eq('new_title') }
+ context 'when params are present' do
+ it 'assigns the params to the milestone' do
+ expect(milestone).to receive(:assign_attributes).with(params.except(:state_event))
- context 'state_event is activate' do
- it 'calls ReopenService' do
- expect(Milestones::ReopenService).to receive(:new).with(project, user, {}).and_return(inner_service)
- expect(inner_service).to receive(:execute).with(milestone)
+ update_milestone.execute(milestone)
+ end
+ end
- described_class.new(project, user, { state_event: 'activate' }).execute(milestone)
- end
+ context 'when milestone is changed' do
+ before do
+ allow(milestone).to receive(:changed?).and_return(true)
end
- context 'state_event is close' do
- it 'calls ReopenService' do
- expect(Milestones::CloseService).to receive(:new).with(project, user, {}).and_return(inner_service)
- expect(inner_service).to receive(:execute).with(milestone)
+ it 'calls before_update' do
+ expect(update_milestone).to receive(:before_update).with(milestone)
- described_class.new(project, user, { state_event: 'close' }).execute(milestone)
- end
+ update_milestone.execute(milestone)
end
end
+
+ context 'when milestone is not changed' do
+ before do
+ allow(milestone).to receive(:changed?).and_return(false)
+ end
+
+ it 'does not call before_update' do
+ expect(update_milestone).not_to receive(:before_update)
+
+ update_milestone.execute(milestone)
+ end
+ end
+
+ it 'saves the milestone' do
+ expect(milestone).to receive(:save)
+
+ update_milestone.execute(milestone)
+ end
+
+ it 'returns the milestone' do
+ expect(update_milestone.execute(milestone)).to eq(milestone)
+ end
+ end
+
+ describe '#before_update' do
+ it 'checks for spam' do
+ expect(milestone).to receive(:check_for_spam).with(user: user, action: :update)
+
+ update_milestone.send(:before_update, milestone)
+ end
end
end
diff --git a/spec/services/namespace_settings/update_service_spec.rb b/spec/services/namespace_settings/update_service_spec.rb
index daffae1dda7..37cbaf19a6e 100644
--- a/spec/services/namespace_settings/update_service_spec.rb
+++ b/spec/services/namespace_settings/update_service_spec.rb
@@ -45,6 +45,22 @@ RSpec.describe NamespaceSettings::UpdateService, feature_category: :groups_and_p
end
end
+ context 'when default_branch_protection is updated' do
+ let(:namespace_settings) { group.namespace_settings }
+ let(:expected) { ::Gitlab::Access::BranchProtection.protected_against_developer_pushes.stringify_keys }
+ let(:settings) { { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it "updates default_branch_protection_defaults from the default_branch_protection param" do
+ expect { service.execute }
+ .to change { namespace_settings.default_branch_protection_defaults }
+ .from({}).to(expected)
+ end
+ end
+
context "updating :resource_access_token_creation_allowed" do
let(:settings) { { resource_access_token_creation_allowed: false } }
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index 0bcfd6b63d2..35d3620e429 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -22,6 +22,9 @@ RSpec.describe Notes::PostProcessService, feature_category: :team_planning do
it do
expect(project).to receive(:execute_hooks)
expect(project).to receive(:execute_integrations)
+ expect_next_instance_of(Integrations::GroupMentionService) do |group_mention_service|
+ expect(group_mention_service).to receive(:execute)
+ end
described_class.new(@note).execute
end
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index cd3a4e8a395..c51e381014d 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -182,7 +182,7 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
context 'on an incident' do
before do
- issue.update!(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident))
+ issue.update!(work_item_type: WorkItems::Type.default_by_type(:incident))
end
it 'leaves the note empty' do
@@ -224,7 +224,7 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
context 'on an incident' do
before do
- issue.update!(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident))
+ issue.update!(work_item_type: WorkItems::Type.default_by_type(:incident))
end
it 'leaves the note empty' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 99f3134f06f..1d1dd045a09 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -532,7 +532,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
allow(::Gitlab::Email::IncomingEmail).to receive(:supports_wildcard?) { true }
end
- let(:subject) { NotificationService.new }
+ let(:subject) { described_class.new }
let(:mailer) { double(deliver_later: true) }
let(:issue) { create(:issue, author: User.support_bot) }
let(:project) { issue.project }
@@ -3889,7 +3889,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
let(:note) { create(:note, noteable: issue, project: project) }
let(:member) { create(:user) }
- subject { NotificationService.new }
+ subject { described_class.new }
before do
project.add_maintainer(member)
diff --git a/spec/services/packages/cleanup/execute_policy_service_spec.rb b/spec/services/packages/cleanup/execute_policy_service_spec.rb
index a083dc0d4ea..249fd50588f 100644
--- a/spec/services/packages/cleanup/execute_policy_service_spec.rb
+++ b/spec/services/packages/cleanup/execute_policy_service_spec.rb
@@ -122,13 +122,13 @@ RSpec.describe Packages::Cleanup::ExecutePolicyService, feature_category: :packa
def mock_service_timeout(on_iteration:)
execute_call_count = 1
expect_next_instances_of(::Packages::MarkPackageFilesForDestructionService, 3) do |service|
- expect(service).to receive(:execute).and_wrap_original do |m, *args|
+ expect(service).to receive(:execute).and_wrap_original do |m, *args, **kwargs|
# timeout if we are on the right iteration
if execute_call_count == on_iteration
service_timeout_response
else
execute_call_count += 1
- m.call(*args)
+ m.call(*args, **kwargs)
end
end
end
diff --git a/spec/services/packages/debian/find_or_create_package_service_spec.rb b/spec/services/packages/debian/find_or_create_package_service_spec.rb
deleted file mode 100644
index c2ae3d56864..00000000000
--- a/spec/services/packages/debian/find_or_create_package_service_spec.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Packages::Debian::FindOrCreatePackageService, feature_category: :package_registry do
- let_it_be(:distribution) { create(:debian_project_distribution, :with_suite) }
- let_it_be(:distribution2) { create(:debian_project_distribution, :with_suite) }
-
- let_it_be(:project) { distribution.project }
- let_it_be(:user) { create(:user) }
-
- let(:service) { described_class.new(project, user, params) }
- let(:params2) { params }
- let(:service2) { described_class.new(project, user, params2) }
-
- let(:package) { subject.payload[:package] }
- let(:package2) { service2.execute.payload[:package] }
-
- shared_examples 'find or create Debian package' do
- it 'returns the same object' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
- expect(subject).to be_success
- expect(package).to be_valid
- expect(package.project_id).to eq(project.id)
- expect(package.creator_id).to eq(user.id)
- expect(package.name).to eq('foo')
- expect(package.version).to eq('1.0+debian')
- expect(package).to be_debian
- expect(package.debian_publication.distribution).to eq(distribution)
-
- expect { package2 }.not_to change { ::Packages::Package.count }
- expect(package2.id).to eq(package.id)
- end
-
- context 'with package marked as pending_destruction' do
- it 'creates a new package' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
-
- package.pending_destruction!
-
- expect { package2 }.to change { ::Packages::Package.count }.by(1)
- expect(package2.id).not_to eq(package.id)
- end
- end
- end
-
- describe '#execute' do
- subject { service.execute }
-
- context 'with a codename as distribution name' do
- let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } }
-
- it_behaves_like 'find or create Debian package'
- end
-
- context 'with a suite as distribution name' do
- let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.suite } }
-
- it_behaves_like 'find or create Debian package'
- end
-
- context 'with existing package in another distribution' do
- let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } }
- let(:params2) { { name: 'foo', version: '1.0+debian', distribution_name: distribution2.codename } }
-
- it 'raises ArgumentError' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
-
- expect { package2 }.to raise_error(ArgumentError, "Debian package #{package.name} #{package.version} exists " \
- "in distribution #{distribution.codename}")
- end
- end
-
- context 'with non-existing distribution' do
- let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: 'not-existing' } }
-
- it 'raises ActiveRecord::RecordNotFound' do
- expect { package }.to raise_error(ActiveRecord::RecordNotFound,
- /^Couldn't find Packages::Debian::ProjectDistribution/)
- end
- end
- end
-end
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
deleted file mode 100644
index 39b917cf1bc..00000000000
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Packages::Debian::ProcessChangesService, feature_category: :package_registry do
- describe '#execute' do
- let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, suite: 'unstable') }
-
- let!(:incoming) { create(:debian_incoming, project: distribution.project, with_changes_file: true) }
-
- let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2_amd64.changes').first }
-
- subject { described_class.new(package_file, user) }
-
- context 'with valid package file' do
- it 'updates package and package file', :aggregate_failures do
- expect(::Packages::Debian::GenerateDistributionWorker).to receive(:perform_async).with(:project, distribution.id)
- 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(8).to(0)
- .and change { package_file.debian_file_metadatum&.reload&.file_type }.from('unknown').to('changes')
-
- created_package = Packages::Package.last
- expect(created_package.name).to eq 'sample'
- expect(created_package.version).to eq '1.2.3~alpha2'
- expect(created_package.creator).to eq user
- end
-
- context 'with non-matching distribution' do
- before do
- distribution.update! suite: FFaker::Lorem.word
- end
-
- it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
- end
-
- context 'with missing field in .changes file' do
- shared_examples 'raises error with missing field' do |missing_field|
- before do
- allow_next_instance_of(::Packages::Debian::ExtractChangesMetadataService) do |extract_changes_metadata_service|
- expect(extract_changes_metadata_service).to receive(:execute).once.and_wrap_original do |m, *args|
- metadata = m.call(*args)
- metadata[:fields].delete(missing_field)
- metadata
- end
- end
- end
-
- it { expect { subject.execute }.to raise_error(ArgumentError, "missing #{missing_field} field") }
- end
-
- it_behaves_like 'raises error with missing field', 'Source'
- it_behaves_like 'raises error with missing field', 'Version'
- it_behaves_like 'raises error with missing field', 'Distribution'
- end
-
- context 'with existing package in the same distribution' do
- let_it_be_with_reload(:existing_package) do
- create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project, published_in: distribution)
- end
-
- it 'does not create a package and assigns the package_file to the existing package' do
- expect { subject.execute }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and change { package_file.package }.to(existing_package)
- end
-
- context 'and marked as pending_destruction' do
- it 'does not re-use the existing package' do
- existing_package.pending_destruction!
-
- expect { subject.execute }
- .to change { Packages::Package.count }.by(1)
- .and not_change { Packages::PackageFile.count }
- end
- end
- end
-
- context 'with existing package in another distribution' do
- let_it_be_with_reload(:existing_package) do
- create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project)
- end
-
- it 'raise ExtractionError' do
- expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
- expect { subject.execute }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and not_change { incoming.package_files.count }
- .and raise_error(ArgumentError,
- "Debian package #{existing_package.name} #{existing_package.version} exists " \
- "in distribution #{existing_package.debian_distribution.codename}")
- end
-
- context 'and marked as pending_destruction' do
- it 'does not re-use the existing package' do
- existing_package.pending_destruction!
-
- expect { subject.execute }
- .to change { Packages::Package.count }.by(1)
- .and not_change { Packages::PackageFile.count }
- end
- end
- end
- end
-
- context 'with invalid package file' do
- let(:package_file) { incoming.package_files.first }
-
- it 'raise ExtractionError', :aggregate_failures do
- expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
- expect { subject.execute }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and not_change { incoming.package_files.count }
- .and raise_error(Packages::Debian::ExtractChangesMetadataService::ExtractionError, 'is not a changes file')
- end
- end
-
- context 'when creating package fails' do
- before do
- allow_next_instance_of(::Packages::Debian::FindOrCreatePackageService) do |find_or_create_package_service|
- expect(find_or_create_package_service).to receive(:execute).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
- end
- end
-
- it 're-raise error', :aggregate_failures do
- expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
- expect { subject.execute }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and not_change { incoming.package_files.count }
- .and raise_error(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
- end
- end
- end
-end
diff --git a/spec/services/packages/npm/create_metadata_cache_service_spec.rb b/spec/services/packages/npm/create_metadata_cache_service_spec.rb
index 02f29dd94df..f4010a7d548 100644
--- a/spec/services/packages/npm/create_metadata_cache_service_spec.rb
+++ b/spec/services/packages/npm/create_metadata_cache_service_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Packages::Npm::CreateMetadataCacheService, :clean_gitlab_redis_sh
new_metadata = Gitlab::Json.parse(npm_metadata_cache.file.read)
expect(new_metadata).not_to eq(metadata)
- expect(new_metadata['dist_tags'].keys).to include(tag_name)
+ expect(new_metadata['dist-tags'].keys).to include(tag_name)
expect(npm_metadata_cache.reload.size).not_to eq(metadata_size)
end
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index a12d86412d8..8b94bce6650 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
let(:namespace) { create(:namespace) }
let(:project) { create(:project, namespace: namespace) }
- let(:user) { create(:user) }
+ let(:user) { project.owner }
let(:version) { '1.0.1' }
let(:params) do
@@ -69,7 +69,7 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
field_sizes: expected_field_sizes
)
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large')
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /structure is too large/)
.and not_change { Packages::Package.count }
.and not_change { Packages::Package.npm.count }
.and not_change { Packages::Tag.count }
@@ -171,6 +171,12 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
it_behaves_like 'valid package'
end
+ context 'when user is no project member' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'valid package'
+ end
+
context 'scoped package not following the naming convention' do
let(:package_name) { '@any-scope/package' }
@@ -290,7 +296,7 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
end
with_them do
- it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid') }
+ it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Version #{Gitlab::Regex.semver_regex_message}") }
end
end
@@ -313,7 +319,7 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
end
it { expect(subject[:http_status]).to eq 400 }
- it { expect(subject[:message]).to eq 'Could not obtain package lease.' }
+ it { expect(subject[:message]).to eq 'Could not obtain package lease. Please try again.' }
end
context 'when many of the same packages are created at the same time', :delete do
diff --git a/spec/services/packages/npm/generate_metadata_service_spec.rb b/spec/services/packages/npm/generate_metadata_service_spec.rb
index 1e3b0f71972..fdd0ab0ccee 100644
--- a/spec/services/packages/npm/generate_metadata_service_spec.rb
+++ b/spec/services/packages/npm/generate_metadata_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
let_it_be(:package2) { create(:npm_package, version: '2.0.6', project: project, name: package_name) }
let_it_be(:latest_package) { create(:npm_package, version: '2.0.11', project: project, name: package_name) }
- let(:packages) { project.packages.npm.with_name(package_name).last_of_each_version }
+ let(:packages) { project.packages.npm.with_name(package_name) }
let(:metadata) { described_class.new(package_name, packages).execute }
describe '#versions' do
@@ -157,14 +157,14 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
end
def check_n_plus_one(only_dist_tags: false)
- pkgs = project.packages.npm.with_name(package_name).last_of_each_version.preload_files
+ pkgs = project.packages.npm.with_name(package_name).preload_files
control = ActiveRecord::QueryRecorder.new do
described_class.new(package_name, pkgs).execute(only_dist_tags: only_dist_tags)
end
yield
- pkgs = project.packages.npm.with_name(package_name).last_of_each_version.preload_files
+ pkgs = project.packages.npm.with_name(package_name).preload_files
expect do
described_class.new(package_name, pkgs).execute(only_dist_tags: only_dist_tags)
diff --git a/spec/services/packages/nuget/extract_metadata_content_service_spec.rb b/spec/services/packages/nuget/extract_metadata_content_service_spec.rb
new file mode 100644
index 00000000000..ff1b26e8b28
--- /dev/null
+++ b/spec/services/packages/nuget/extract_metadata_content_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::ExtractMetadataContentService, feature_category: :package_registry do
+ let(:nuspec_file_content) { fixture_file(nuspec_filepath) }
+
+ let(:service) { described_class.new(nuspec_file_content) }
+
+ describe '#execute' do
+ subject { service.execute.payload }
+
+ context 'with nuspec file content' do
+ context 'with dependencies' do
+ let(:nuspec_filepath) { 'packages/nuget/with_dependencies.nuspec' }
+
+ it { is_expected.to have_key(:package_dependencies) }
+
+ it 'extracts dependencies' do
+ dependencies = subject[:package_dependencies]
+
+ expect(dependencies).to include(name: 'Moqi', version: '2.5.6')
+ expect(dependencies).to include(name: 'Castle.Core')
+ expect(dependencies).to include(name: 'Test.Dependency', version: '2.3.7',
+ target_framework: '.NETStandard2.0')
+ expect(dependencies).to include(name: 'Newtonsoft.Json', version: '12.0.3',
+ target_framework: '.NETStandard2.0')
+ end
+ end
+
+ context 'with package types' do
+ let(:nuspec_filepath) { 'packages/nuget/with_package_types.nuspec' }
+
+ it { is_expected.to have_key(:package_types) }
+
+ it 'extracts package types' do
+ expect(subject[:package_types]).to include('SymbolsPackage')
+ end
+ end
+
+ context 'with a nuspec file with metadata' do
+ let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
+
+ it { expect(subject[:package_tags].sort).to eq(%w[foo bar test tag1 tag2 tag3 tag4 tag5].sort) }
+ end
+ end
+
+ context 'with a nuspec file content with metadata' do
+ let_it_be(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
+
+ it 'returns the correct metadata' do
+ expected_metadata = {
+ authors: 'Author Test',
+ description: 'Description Test',
+ license_url: 'https://opensource.org/licenses/MIT',
+ project_url: 'https://gitlab.com/gitlab-org/gitlab',
+ icon_url: 'https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png'
+ }
+
+ expect(subject.slice(*expected_metadata.keys)).to eq(expected_metadata)
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/extract_metadata_file_service_spec.rb b/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
new file mode 100644
index 00000000000..412c22fe8de
--- /dev/null
+++ b/spec/services/packages/nuget/extract_metadata_file_service_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Nuget::ExtractMetadataFileService, feature_category: :package_registry do
+ let_it_be(:package_file) { create(:nuget_package).package_files.first }
+
+ let(:service) { described_class.new(package_file.id) }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ shared_examples 'raises an error' do |error_message|
+ it { expect { subject }.to raise_error(described_class::ExtractionError, error_message) }
+ end
+
+ context 'with valid package file id' do
+ expected_metadata = <<~XML.squish
+ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>DummyProject.DummyPackage</id>
+ <version>1.0.0</version>
+ <title>Dummy package</title>
+ <authors>Test</authors>
+ <owners>Test</owners>
+ <requireLicenseAcceptance>false</requireLicenseAcceptance>
+ <description>This is a dummy project</description>
+ <dependencies>
+ <group targetFramework=".NETCoreApp3.0">
+ <dependency id="Newtonsoft.Json" version="12.0.3" exclude="Build,Analyzers" />
+ </group>
+ </dependencies>
+ </metadata>
+ </package>
+ XML
+
+ it 'returns the nuspec file content' do
+ expect(subject.payload.squish).to include(expected_metadata)
+ end
+ end
+
+ context 'with invalid package file id' do
+ let(:package_file) { instance_double('Packages::PackageFile', id: 555) }
+
+ it_behaves_like 'raises an error', 'invalid package file'
+ end
+
+ context 'when linked to a non nuget package' do
+ before do
+ package_file.package.maven!
+ end
+
+ it_behaves_like 'raises an error', 'invalid package file'
+ end
+
+ context 'with a 0 byte package file id' do
+ before do
+ allow_next_instance_of(Packages::PackageFileUploader) do |instance|
+ allow(instance).to receive(:size).and_return(0)
+ end
+ end
+
+ it_behaves_like 'raises an error', 'invalid package file'
+ end
+
+ context 'without the nuspec file' do
+ before do
+ allow_next_instance_of(Zip::File) do |instance|
+ allow(instance).to receive(:glob).and_return([])
+ end
+ end
+
+ it_behaves_like 'raises an error', 'nuspec file not found'
+ end
+
+ context 'with a too big nuspec file' do
+ before do
+ allow_next_instance_of(Zip::File) do |instance|
+ allow(instance).to receive(:glob).and_return([instance_double('File', size: 6.megabytes)])
+ end
+ end
+
+ it_behaves_like 'raises an error', 'nuspec file too big'
+ end
+
+ context 'with a corrupted nupkg file with a wrong entry size' do
+ let(:nupkg_fixture_path) { expand_fixture_path('packages/nuget/corrupted_package.nupkg') }
+
+ before do
+ allow(Zip::File).to receive(:new).and_return(Zip::File.new(nupkg_fixture_path, false, false))
+ end
+
+ it_behaves_like 'raises an error',
+ <<~ERROR.squish
+ nuspec file has the wrong entry size: entry 'DummyProject.DummyPackage.nuspec' should be 255B,
+ but is larger when inflated.
+ ERROR
+ end
+ end
+end
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 8954b89971e..c8c06414830 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -5,17 +5,33 @@ require 'spec_helper'
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) }
+ subject { described_class.new(package_file.id) }
describe '#execute' do
- subject { service.execute }
-
- shared_examples 'raises an error' do |error_message|
- it { expect { subject }.to raise_error(described_class::ExtractionError, error_message) }
+ let(:nuspec_file_content) do
+ <<~XML.squish
+ <?xml version="1.0" encoding="utf-8"?>
+ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>DummyProject.DummyPackage</id>
+ <version>1.0.0</version>
+ <title>Dummy package</title>
+ <authors>Test</authors>
+ <owners>Test</owners>
+ <requireLicenseAcceptance>false</requireLicenseAcceptance>
+ <description>This is a dummy project</description>
+ <dependencies>
+ <group targetFramework=".NETCoreApp3.0">
+ <dependency id="Newtonsoft.Json" version="12.0.3" exclude="Build,Analyzers" />
+ </group>
+ </dependencies>
+ </metadata>
+ </package>
+ XML
end
- context 'with valid package file id' do
- expected_metadata = {
+ let(:expected_metadata) do
+ {
package_name: 'DummyProject.DummyPackage',
package_version: '1.0.0',
authors: 'Test',
@@ -30,113 +46,21 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
package_tags: [],
package_types: []
}
-
- it { is_expected.to eq(expected_metadata) }
end
- context 'with nuspec file' do
- before do
- allow(service).to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
- end
-
- context 'with dependencies' do
- let(:nuspec_filepath) { 'packages/nuget/with_dependencies.nuspec' }
-
- it { is_expected.to have_key(:package_dependencies) }
-
- it 'extracts dependencies' do
- dependencies = subject[:package_dependencies]
-
- expect(dependencies).to include(name: 'Moqi', version: '2.5.6')
- expect(dependencies).to include(name: 'Castle.Core')
- expect(dependencies).to include(name: 'Test.Dependency', version: '2.3.7', target_framework: '.NETStandard2.0')
- expect(dependencies).to include(name: 'Newtonsoft.Json', version: '12.0.3', target_framework: '.NETStandard2.0')
- end
- end
-
- context 'with package types' do
- let(:nuspec_filepath) { 'packages/nuget/with_package_types.nuspec' }
-
- it { is_expected.to have_key(:package_types) }
-
- it 'extracts package types' do
- expect(subject[:package_types]).to include('SymbolsPackage')
+ it 'calls the necessary services and executes the metadata extraction' do
+ expect(::Packages::Nuget::ExtractMetadataFileService).to receive(:new).with(package_file.id) do
+ double.tap do |service|
+ expect(service).to receive(:execute).and_return(double(payload: nuspec_file_content))
end
end
- context 'with a nuspec file with metadata' do
- let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
-
- it { expect(subject[:package_tags].sort).to eq(%w(foo bar test tag1 tag2 tag3 tag4 tag5).sort) }
- end
- end
-
- context 'with a nuspec file with metadata' do
- let_it_be(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
-
- before do
- allow(service).to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
- end
-
- it 'returns the correct metadata' do
- expected_metadata = {
- authors: 'Author Test',
- description: 'Description Test',
- license_url: 'https://opensource.org/licenses/MIT',
- project_url: 'https://gitlab.com/gitlab-org/gitlab',
- icon_url: 'https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png'
- }
-
- expect(subject.slice(*expected_metadata.keys)).to eq(expected_metadata)
- end
- end
+ expect(::Packages::Nuget::ExtractMetadataContentService).to receive_message_chain(:new, :execute)
+ .with(nuspec_file_content).with(no_args).and_return(double(payload: expected_metadata))
- context 'with invalid package file id' do
- let(:package_file) { double('file', id: 555) }
-
- it_behaves_like 'raises an error', 'invalid package file'
- end
-
- context 'linked to a non nuget package' do
- before do
- package_file.package.maven!
- end
-
- it_behaves_like 'raises an error', 'invalid package file'
- end
-
- context 'with a 0 byte package file id' do
- before do
- allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(0)
- end
-
- it_behaves_like 'raises an error', 'invalid package file'
- end
-
- context 'without the nuspec file' do
- before do
- allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
- end
-
- it_behaves_like 'raises an error', 'nuspec file not found'
- end
-
- context 'with a too big nuspec file' do
- before do
- allow_any_instance_of(Zip::File).to receive(:glob).and_return([double('file', size: 6.megabytes)])
- end
-
- it_behaves_like 'raises an error', 'nuspec file too big'
- end
-
- context 'with a corrupted nupkg file with a wrong entry size' do
- let(:nupkg_fixture_path) { expand_fixture_path('packages/nuget/corrupted_package.nupkg') }
-
- before do
- allow(Zip::File).to receive(:new).and_return(Zip::File.new(nupkg_fixture_path, false, false))
- end
+ metadata = subject.execute.payload
- it_behaves_like 'raises an error', "nuspec file has the wrong entry size: entry 'DummyProject.DummyPackage.nuspec' should be 255B, but is larger when inflated."
+ expect(metadata).to eq(expected_metadata)
end
end
end
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 fa7d994c13c..caa4e42d002 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
@@ -228,7 +228,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
end
end
- it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError, with_message: 'nuspec file not found'
+ it_behaves_like 'raising an', ::Packages::Nuget::ExtractMetadataFileService::ExtractionError, with_message: 'nuspec file not found'
end
context 'with a symbol package' do
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 77ea5e10379..32544b5d6b4 100644
--- a/spec/services/personal_access_tokens/last_used_service_spec.rb
+++ b/spec/services/personal_access_tokens/last_used_service_spec.rb
@@ -43,49 +43,5 @@ RSpec.describe PersonalAccessTokens::LastUsedService, feature_category: :system_
expect(subject).to be_nil
end
end
-
- context 'when update_personal_access_token_usage_information_every_10_minutes is disabled' do
- before do
- stub_feature_flags(update_personal_access_token_usage_information_every_10_minutes: false)
- end
-
- context 'when the personal access token was used 1 day ago', :freeze_time do
- let(:personal_access_token) { create(:personal_access_token, last_used_at: 1.day.ago) }
-
- it 'updates the last_used_at timestamp' do
- expect { subject }.to change { personal_access_token.last_used_at }
- end
-
- it 'does not run on read-only GitLab instances' do
- allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
-
- expect { subject }.not_to change { personal_access_token.last_used_at }
- end
- end
-
- context 'when the personal access token was used less than 1 day ago', :freeze_time do
- let(:personal_access_token) { create(:personal_access_token, last_used_at: (1.day - 1.second).ago) }
-
- it 'does not update the last_used_at timestamp' do
- expect { subject }.not_to change { personal_access_token.last_used_at }
- end
- end
-
- context 'when the last_used_at timestamp is nil' do
- let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: nil) }
-
- it 'updates the last_used_at timestamp' do
- expect { subject }.to change { personal_access_token.last_used_at }
- end
- end
-
- context 'when not a personal access token' do
- let_it_be(:personal_access_token) { create(:oauth_access_token) }
-
- it 'does not execute' do
- expect(subject).to be_nil
- end
- end
- end
end
end
diff --git a/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb b/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb
new file mode 100644
index 00000000000..3e32200cc77
--- /dev/null
+++ b/spec/services/personal_access_tokens/revoke_token_family_service_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PersonalAccessTokens::RevokeTokenFamilyService, feature_category: :system_access do
+ describe '#execute' do
+ let_it_be(:token_3) { create(:personal_access_token, :revoked) }
+ let_it_be(:token_2) { create(:personal_access_token, :revoked, previous_personal_access_token_id: token_3.id) }
+ let_it_be(:token_1) { create(:personal_access_token, previous_personal_access_token_id: token_2.id) }
+
+ subject(:response) { described_class.new(token_3).execute }
+
+ it 'revokes the latest token from the chain of rotated tokens' do
+ expect(response).to be_success
+ expect(token_1.reload).to be_revoked
+ end
+ end
+end
diff --git a/spec/services/personal_access_tokens/rotate_service_spec.rb b/spec/services/personal_access_tokens/rotate_service_spec.rb
index e026b0b6485..522506870f6 100644
--- a/spec/services/personal_access_tokens/rotate_service_spec.rb
+++ b/spec/services/personal_access_tokens/rotate_service_spec.rb
@@ -25,6 +25,13 @@ RSpec.describe PersonalAccessTokens::RotateService, feature_category: :system_ac
expect(new_token).not_to be_revoked
end
+ it 'saves the previous token as previous PAT attribute' do
+ response
+
+ new_token = response.payload[:personal_access_token]
+ expect(new_token.previous_personal_access_token).to eql(token)
+ end
+
context 'when user tries to rotate already revoked token' do
let_it_be(:token, reload: true) { create(:personal_access_token, :revoked) }
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 59db0b47a3c..8a737e4df56 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -1030,17 +1030,17 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
end
before do
- group.update_shared_runners_setting!(shared_runners_setting)
+ group.update!(shared_runners_enabled: shared_runners_enabled,
+ allow_descendants_override_disabled_shared_runners: allow_to_override)
user.refresh_authorized_projects # Ensure cache is warm
end
context 'default value based on parent group setting' do
- where(:shared_runners_setting, :desired_config_for_new_project, :expected_result_for_project) do
- Namespace::SR_ENABLED | nil | true
- Namespace::SR_DISABLED_WITH_OVERRIDE | nil | false
- Namespace::SR_DISABLED_AND_OVERRIDABLE | nil | false
- Namespace::SR_DISABLED_AND_UNOVERRIDABLE | nil | false
+ where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project, :expected_result_for_project) do
+ true | false | nil | true
+ false | true | nil | false
+ false | false | nil | false
end
with_them do
@@ -1057,14 +1057,12 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
end
context 'parent group is present and allows desired config' do
- where(:shared_runners_setting, :desired_config_for_new_project, :expected_result_for_project) do
- Namespace::SR_ENABLED | true | true
- Namespace::SR_ENABLED | false | false
- Namespace::SR_DISABLED_WITH_OVERRIDE | false | false
- Namespace::SR_DISABLED_WITH_OVERRIDE | true | true
- Namespace::SR_DISABLED_AND_OVERRIDABLE | false | false
- Namespace::SR_DISABLED_AND_OVERRIDABLE | true | true
- Namespace::SR_DISABLED_AND_UNOVERRIDABLE | false | false
+ where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project, :expected_result_for_project) do
+ true | false | true | true
+ true | false | false | false
+ false | true | false | false
+ false | true | true | true
+ false | false | false | false
end
with_them do
@@ -1080,8 +1078,8 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
end
context 'parent group is present and disallows desired config' do
- where(:shared_runners_setting, :desired_config_for_new_project) do
- Namespace::SR_DISABLED_AND_UNOVERRIDABLE | true
+ where(:shared_runners_enabled, :allow_to_override, :desired_config_for_new_project) do
+ false | false | true
end
with_them do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 7aa6980fb24..ccf58964c71 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -456,14 +456,63 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
context 'repository removal' do
- # 1. Project repository
- # 2. Wiki repository
- it 'removal of existing repos' do
- expect_next_instances_of(Repositories::DestroyService, 2) do |instance|
- expect(instance).to receive(:execute).and_return(status: :success)
+ describe '.trash_project_repositories!' do
+ let(:trash_project_repositories!) { described_class.new(project, user, {}).send(:trash_project_repositories!) }
+
+ # Destroys 3 repositories:
+ # 1. Project repository
+ # 2. Wiki repository
+ # 3. Design repository
+
+ it 'Repositories::DestroyService is called for existing repos' do
+ expect_next_instances_of(Repositories::DestroyService, 3) do |instance|
+ expect(instance).to receive(:execute).and_return(status: :success)
+ end
+
+ trash_project_repositories!
end
- described_class.new(project, user, {}).execute
+ context 'when the removal has errors' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:mock_error) { instance_double(Repositories::DestroyService, execute: { message: 'foo', status: :error }) }
+ let(:project_repository) { project.repository }
+ let(:wiki_repository) { project.wiki.repository }
+ let(:design_repository) { project.design_repository }
+
+ where(:repo, :message) do
+ ref(:project_repository) | 'Failed to remove project repository. Please try again or contact administrator.'
+ ref(:wiki_repository) | 'Failed to remove wiki repository. Please try again or contact administrator.'
+ ref(:design_repository) | 'Failed to remove design repository. Please try again or contact administrator.'
+ end
+
+ with_them do
+ before do
+ allow(Repositories::DestroyService).to receive(:new).with(anything).and_call_original
+ allow(Repositories::DestroyService).to receive(:new).with(repo).and_return(mock_error)
+ end
+
+ it 'raises correct error' do
+ expect { trash_project_repositories! }.to raise_error(Projects::DestroyService::DestroyError, message)
+ end
+ end
+ end
+ end
+
+ it 'removes project repository' do
+ expect { destroy_project(project, user, {}) }.to change { project.repository.exists? }.from(true).to(false)
+ end
+
+ it 'removes wiki repository' do
+ project.create_wiki unless project.wiki.repository.exists?
+
+ expect { destroy_project(project, user, {}) }.to change { project.wiki.repository.exists? }.from(true).to(false)
+ end
+
+ it 'removes design repository' do
+ project.design_repository.create_if_not_exists
+
+ expect { destroy_project(project, user, {}) }.to change { project.design_repository.exists? }.from(true).to(false)
end
end
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index e062ee04bf4..a0b14a36106 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Projects::DownloadService, feature_category: :groups_and_projects
@project = create(:project, creator_id: @user.id, namespace: @user.namespace)
end
- context 'for a URL that is not on whitelist' do
+ context 'for a URL that is not on allowlist' do
before do
url = 'https://code.jquery.com/jquery-2.1.4.min.js'
@link_to_file = download_file(@project, url)
@@ -18,7 +18,7 @@ RSpec.describe Projects::DownloadService, feature_category: :groups_and_projects
it { expect(@link_to_file).to eq(nil) }
end
- context 'for URLs that are on the whitelist' do
+ context 'for URLs that are on the allowlist' do
before do
# `ssrf_filter` resolves the hostname. See https://github.com/carrierwaveuploader/carrierwave/commit/91714adda998bc9e8decf5b1f5d260d808761304
stub_request(:get, %r{http://[\d.]+/rails_sample.jpg}).to_return(body: File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'))
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 2f090577805..04c43dff2dc 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -10,12 +10,18 @@ RSpec.describe Projects::ParticipantsService, feature_category: :groups_and_proj
before_all do
project.add_developer(user)
+
+ stub_feature_flags(disable_all_mention: false)
end
def run_service
described_class.new(project, user).execute(noteable)
end
+ it 'includes `All Project and Group Members`' do
+ expect(run_service).to include(a_hash_including({ username: "all", name: "All Project and Group Members" }))
+ end
+
context 'N+1 checks' do
before do
run_service # warmup, runs table cache queries and create queries
@@ -99,6 +105,16 @@ RSpec.describe Projects::ParticipantsService, feature_category: :groups_and_proj
end
end
end
+
+ context 'when `disable_all_mention` FF is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+ end
+
+ it 'does not include `All Project and Group Members`' do
+ expect(run_service).not_to include(a_hash_including({ username: "all", name: "All Project and Group Members" }))
+ end
+ end
end
describe '#project_members' do
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index badbc8b628e..bfcd2be6ce4 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -690,7 +690,7 @@ RSpec.describe Projects::UpdateService, feature_category: :groups_and_projects d
attributes_for(
:prometheus_integration,
project: project,
- properties: { api_url: nil, manual_configuration: "1" }
+ properties: { api_url: 'invalid-url', manual_configuration: "1" }
)
end
diff --git a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
index fbee4b9c7d7..a5395eed1b4 100644
--- a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
+++ b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Prometheus::ProxyVariableSubstitutionService, feature_category: :
end
it_behaves_like 'success' do
- let(:expected_query) { %Q[up{environment="#{environment.slug}"}] }
+ let(:expected_query) { %[up{environment="#{environment.slug}"}] }
end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index bd09dae0a5a..186b532233e 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -35,6 +35,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
describe '#execute' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
shared_examples 'reopen command' do
@@ -301,7 +302,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'returns due_date message: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq("Set the due date to #{expected_date.to_s(:medium)}.")
+ expect(message).to eq("Set the due date to #{expected_date.to_fs(:medium)}.")
end
end
@@ -538,7 +539,12 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
_, updates, message = service.execute(content, issuable)
expect(updates).to eq(merge: merge_request.diff_head_sha)
- expect(message).to eq('Scheduled to merge this merge request (Merge when pipeline succeeds).')
+
+ if Gitlab.ee?
+ expect(message).to eq('Scheduled to merge this merge request (Merge when checks pass).')
+ else
+ expect(message).to eq('Scheduled to merge this merge request (Merge when pipeline succeeds).')
+ end
end
end
@@ -1369,6 +1375,11 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
let(:issuable) { merge_request }
end
+ it_behaves_like 'done command' do
+ let(:content) { '/done' }
+ let(:issuable) { work_item }
+ end
+
it_behaves_like 'subscribe command' do
let(:content) { '/subscribe' }
let(:issuable) { issue }
@@ -1590,6 +1601,12 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
+ context 'if issuable is a work item' do
+ it_behaves_like 'todo command' do
+ let(:issuable) { work_item }
+ end
+ end
+
context 'if issuable is a MergeRequest' do
it_behaves_like 'todo command' do
let(:issuable) { merge_request }
@@ -2860,13 +2877,13 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
let_it_be(:project) { create(:project, :private) }
let_it_be(:work_item) { create(:work_item, :task, project: project) }
- let(:command) { '/type Issue' }
+ let(:command) { '/type issue' }
it 'has command available' do
_, explanations = service.explain(command, work_item)
expect(explanations)
- .to contain_exactly("Converts work item to Issue. Widgets not supported in new type are removed.")
+ .to contain_exactly("Converts work item to issue. Widgets not supported in new type are removed.")
end
end
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index 31e4e008d4f..940d9858cdd 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -199,16 +199,14 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
end
end
- context 'expiry of the project bot member' do
- it 'project bot membership does not expire' do
- response = subject
- access_token = response.payload[:access_token]
- project_bot = access_token.user
+ it 'project bot membership expires when PAT expires' do
+ response = subject
+ access_token = response.payload[:access_token]
+ project_bot = access_token.user
- expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq(
- max_pat_access_token_lifetime.to_date
- )
- end
+ expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq(
+ max_pat_access_token_lifetime.to_date
+ )
end
end
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 3396abaff9e..7c1c5884fd2 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
@@ -13,7 +13,7 @@ RSpec.describe ResourceEvents::SyntheticLabelNotesBuilderService, feature_catego
let_it_be(:event3) { create(:resource_label_event, issue: issue) }
it 'returns the expected synthetic notes' do
- notes = ResourceEvents::SyntheticLabelNotesBuilderService.new(issue, user).execute
+ notes = described_class.new(issue, user).execute
expect(notes.size).to eq(3)
end
diff --git a/spec/services/service_desk/custom_emails/create_service_spec.rb b/spec/services/service_desk/custom_emails/create_service_spec.rb
new file mode 100644
index 00000000000..0d9582ba235
--- /dev/null
+++ b/spec/services/service_desk/custom_emails/create_service_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :service_desk do
+ describe '#execute' do
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new(project: project, current_user: user, params: params) }
+ let(:error_feature_flag_disabled) { 'Feature flag service_desk_custom_email is not enabled' }
+ let(:error_user_not_authorized) { s_('ServiceDesk|User cannot manage project.') }
+ let(:error_cannot_create_custom_email) { s_('ServiceDesk|Cannot create custom email') }
+ let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') }
+ let(:error_params_missing) { s_('ServiceDesk|Parameters missing') }
+ let(:expected_error_message) { nil }
+ let(:params) { {} }
+ let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) }
+ let(:message) { instance_double(Mail::Message) }
+
+ shared_examples 'a service that exits with error' do
+ it 'exits early' do
+ response = service.execute
+
+ expect(response).to be_error
+ expect(response.message).to eq(expected_error_message)
+ end
+ end
+
+ shared_examples 'a failing service that does not create records' do
+ it 'exits with error and does not create records' do
+ response = service.execute
+ project.reset
+
+ expect(response).to be_error
+ expect(response.message).to eq(expected_error_message)
+ expect(project.service_desk_custom_email_verification).to be nil
+ expect(project.service_desk_custom_email_credential).to be nil
+ expect(project.service_desk_setting).to have_attributes(
+ custom_email: nil,
+ custom_email_enabled: false
+ )
+ end
+ end
+
+ context 'when feature flag service_desk_custom_email is disabled' do
+ let(:expected_error_message) { error_feature_flag_disabled }
+
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it_behaves_like 'a service that exits with error'
+ end
+
+ context 'with illegitimate user' do
+ let(:expected_error_message) { error_user_not_authorized }
+
+ before do
+ stub_member_access_level(project, developer: user)
+ end
+
+ it_behaves_like 'a service that exits with error'
+ end
+
+ context 'with legitimate user' do
+ let!(:settings) { create(:service_desk_setting, project: project) }
+
+ let(:expected_error_message) { error_params_missing }
+
+ before do
+ stub_member_access_level(project, maintainer: user)
+
+ # We send verification email directly and it will fail with
+ # smtp.example.com because it expects a valid DNS record
+ allow(message).to receive(:deliver)
+ allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message)
+ end
+
+ it_behaves_like 'a service that exits with error'
+
+ context 'with params but custom_email missing' do
+ let(:params) do
+ {
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: 'user@example.com',
+ smtp_password: 'supersecret'
+ }
+ end
+
+ it_behaves_like 'a failing service that does not create records'
+ end
+
+ context 'with params but smtp username empty' do
+ let(:params) do
+ {
+ custom_email: 'user@example.com',
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: nil,
+ smtp_password: 'supersecret'
+ }
+ end
+
+ it_behaves_like 'a failing service that does not create records'
+ end
+
+ context 'with params but smtp password is too short' do
+ let(:expected_error_message) { error_cannot_create_custom_email }
+ let(:params) do
+ {
+ custom_email: 'user@example.com',
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: 'user@example.com',
+ smtp_password: '2short'
+ }
+ end
+
+ it_behaves_like 'a failing service that does not create records'
+ end
+
+ context 'with params but custom_email is invalid' do
+ let(:expected_error_message) { error_cannot_create_custom_email }
+ let(:params) do
+ {
+ custom_email: 'useratexampledotcom',
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: 'user@example.com',
+ smtp_password: 'supersecret'
+ }
+ end
+
+ it_behaves_like 'a failing service that does not create records'
+ end
+
+ context 'with full set of params' do
+ let(:params) do
+ {
+ custom_email: 'user@example.com',
+ smtp_address: 'smtp.example.com',
+ smtp_port: '587',
+ smtp_username: 'user@example.com',
+ smtp_password: 'supersecret'
+ }
+ end
+
+ it 'creates all records returns a successful response' do
+ response = service.execute
+ project.reset
+
+ expect(response).to be_success
+
+ expect(project.service_desk_setting).to have_attributes(
+ custom_email: params[:custom_email],
+ custom_email_enabled: false
+ )
+ expect(project.service_desk_custom_email_credential).to have_attributes(
+ smtp_address: params[:smtp_address],
+ smtp_port: params[:smtp_port].to_i,
+ smtp_username: params[:smtp_username],
+ smtp_password: params[:smtp_password]
+ )
+ expect(project.service_desk_custom_email_verification).to have_attributes(
+ state: 'started',
+ triggerer: user,
+ error: nil
+ )
+ end
+
+ context 'when custom email aready exists' do
+ let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+ let!(:verification) { create(:service_desk_custom_email_verification, project: project) }
+
+ let(:expected_error_message) { error_custom_email_exists }
+
+ it_behaves_like 'a service that exits with error'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/service_desk/custom_emails/destroy_service_spec.rb b/spec/services/service_desk/custom_emails/destroy_service_spec.rb
new file mode 100644
index 00000000000..f5a22e26865
--- /dev/null
+++ b/spec/services/service_desk/custom_emails/destroy_service_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmails::DestroyService, feature_category: :service_desk do
+ describe '#execute' do
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:user) { build_stubbed(:user) }
+ let(:service) { described_class.new(project: project, current_user: user) }
+ let(:error_feature_flag_disabled) { 'Feature flag service_desk_custom_email is not enabled' }
+ let(:error_user_not_authorized) { s_('ServiceDesk|User cannot manage project.') }
+ let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') }
+ let(:expected_error_message) { nil }
+
+ shared_examples 'a service that exits with error' do
+ it 'exits early' do
+ response = service.execute
+
+ expect(response).to be_error
+ expect(response.message).to eq(expected_error_message)
+ end
+ end
+
+ shared_examples 'a successful service that destroys all custom email records' do
+ it 'ensures no custom email records exist' do
+ project.reset
+
+ response = service.execute
+
+ expect(response).to be_success
+ expect(project.service_desk_custom_email_verification).to be nil
+ expect(project.service_desk_custom_email_credential).to be nil
+ expect(project.service_desk_setting).to have_attributes(
+ custom_email: nil,
+ custom_email_enabled: false
+ )
+ end
+ end
+
+ context 'when feature flag service_desk_custom_email is disabled' do
+ let(:expected_error_message) { error_feature_flag_disabled }
+
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it_behaves_like 'a service that exits with error'
+ end
+
+ context 'with illegitimate user' do
+ let(:expected_error_message) { error_user_not_authorized }
+
+ before do
+ stub_member_access_level(project, developer: user)
+ end
+
+ it_behaves_like 'a service that exits with error'
+ end
+
+ context 'with legitimate user' do
+ let(:expected_error_message) { error_does_not_exist }
+
+ before do
+ stub_member_access_level(project, maintainer: user)
+ end
+
+ it_behaves_like 'a service that exits with error'
+
+ context 'when service desk setting exists' do
+ let!(:settings) { create(:service_desk_setting, project: project) }
+
+ it_behaves_like 'a successful service that destroys all custom email records'
+
+ context 'when custom email is present' do
+ let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+
+ it_behaves_like 'a successful service that destroys all custom email records'
+
+ context 'when credential exists' do
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+
+ it_behaves_like 'a successful service that destroys all custom email records'
+
+ context 'when verification exists' do
+ let!(:verification) { create(:service_desk_custom_email_verification, project: project) }
+
+ it_behaves_like 'a successful service that destroys all custom email records'
+ end
+ end
+ end
+ 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 342fb2b6b7a..ff564963677 100644
--- a/spec/services/service_desk_settings/update_service_spec.rb
+++ b/spec/services/service_desk_settings/update_service_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
let(:params) { { outgoing_name: 'some name', project_key: 'foo' } }
it 'updates service desk settings' do
- result = described_class.new(settings.project, user, params).execute
+ response = described_class.new(settings.project, user, params).execute
- expect(result[:status]).to eq :success
+ expect(response).to be_success
expect(settings.reload.outgoing_name).to eq 'some name'
expect(settings.reload.project_key).to eq 'foo'
end
@@ -22,9 +22,9 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
let(:params) { { project_key: '' } }
it 'sets nil project_key' do
- result = described_class.new(settings.project, user, params).execute
+ response = described_class.new(settings.project, user, params).execute
- expect(result[:status]).to eq :success
+ expect(response).to be_success
expect(settings.reload.project_key).to be_nil
end
end
@@ -33,10 +33,10 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de
let(:params) { { outgoing_name: 'x' * 256 } }
it 'does not update service desk settings' do
- result = described_class.new(settings.project, user, params).execute
+ response = described_class.new(settings.project, user, params).execute
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'Outgoing name is too long (maximum is 255 characters)'
+ expect(response).to be_error
+ expect(response.message).to eq 'Outgoing name is too long (maximum is 255 characters)'
expect(settings.reload.outgoing_name).to eq 'original name'
end
end
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index 6171ca1a8a6..03fcc11b6bd 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -214,4 +214,17 @@ RSpec.describe ServiceResponse, feature_category: :shared do
end
end
end
+
+ describe '#deconstruct_keys' do
+ it 'supports pattern matching' do
+ status =
+ case described_class.error(message: 'Bad apple')
+ in { status: Symbol => status }
+ status
+ else
+ raise
+ end
+ expect(status).to eq(:error)
+ end
+ end
end
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index b428897ce27..f36560480e3 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Snippets::UpdateService, feature_category: :source_code_managemen
let(:options) { base_opts.merge(extra_opts) }
let(:updater) { user }
- let(:service) { Snippets::UpdateService.new(project: project, current_user: updater, params: options, perform_spam_check: true) }
+ let(:service) { described_class.new(project: project, current_user: updater, params: options, perform_spam_check: true) }
subject { service.execute(snippet) }
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
index 15cb4977b61..bc73a5cbfaf 100644
--- a/spec/services/spam/spam_action_service_spec.rb
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency do
+RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do
include_context 'includes Spam constants'
let(:issue) { create(:issue, project: project, author: author) }
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index 6b14cf33041..70f43d82ead 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -136,6 +136,38 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
end
end
+ context 'if allow_possible_spam user custom attribute is set' do
+ before do
+ UserCustomAttribute.upsert_custom_attributes(
+ [{
+ user_id: user.id,
+ key: 'allow_possible_spam',
+ value: 'does not matter'
+ }]
+ )
+ end
+
+ context 'and a service returns a verdict that should be overridden' do
+ before do
+ allow(service).to receive(:get_spamcheck_verdict).and_return(BLOCK_USER)
+ end
+
+ it 'overrides and renders the override verdict' do
+ is_expected.to eq OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM
+ end
+ end
+
+ context 'and a service returns a verdict that does not need to be overridden' do
+ before do
+ allow(service).to receive(:get_spamcheck_verdict).and_return(ALLOW)
+ end
+
+ it 'does not override and renders the original verdict' do
+ is_expected.to eq ALLOW
+ end
+ end
+ end
+
context 'records metrics' do
let(:histogram) { instance_double(Prometheus::Client::Histogram) }
@@ -237,6 +269,8 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
end
context 'if the endpoint is accessible' do
+ let(:user_scores) { Abuse::UserTrustScore.new(user) }
+
before do
allow(service).to receive(:spamcheck_client).and_return(spam_client)
allow(spam_client).to receive(:spam?).and_return(spam_client_result)
@@ -248,7 +282,7 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
it 'returns the verdict' do
is_expected.to eq(NOOP)
- expect(user.spam_score).to eq(0.0)
+ expect(user_scores.spam_score).to eq(0.0)
end
end
@@ -259,7 +293,7 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
context 'the result was evaluated' do
it 'returns the verdict and updates the spam score' do
is_expected.to eq(ALLOW)
- expect(user.spam_score).to be_within(0.000001).of(verdict_score)
+ expect(user_scores.spam_score).to be_within(0.000001).of(verdict_score)
end
end
@@ -268,7 +302,7 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
it 'returns the verdict and does not update the spam score' do
expect(subject).to eq(ALLOW)
- expect(user.spam_score).to eq(0.0)
+ expect(user_scores.spam_score).to eq(0.0)
end
end
end
@@ -290,7 +324,7 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
with_them do
it "returns expected spam constant and updates the spam score" do
is_expected.to eq(expected)
- expect(user.spam_score).to be_within(0.000001).of(verdict_score)
+ expect(user_scores.spam_score).to be_within(0.000001).of(verdict_score)
end
end
end
@@ -369,7 +403,24 @@ RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency
describe 'issue' do
let(:target) { issue }
- it_behaves_like 'execute spam verdict service'
+ context 'when issue is publicly visible' do
+ before do
+ allow(issue).to receive(:publicly_visible?).and_return(true)
+ end
+
+ it_behaves_like 'execute spam verdict service'
+ end
+
+ context 'when issue is not publicly visible' do
+ before do
+ allow(issue).to receive(:publicly_visible?).and_return(false)
+ allow(service).to receive(:get_spamcheck_verdict).and_return(BLOCK_USER)
+ end
+
+ it 'overrides and renders the override verdict' do
+ expect(service.execute).to eq OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM
+ end
+ end
end
describe 'snippet' do
diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb
index 71228050085..a3793880ff1 100644
--- a/spec/services/system_notes/time_tracking_service_spec.rb
+++ b/spec/services/system_notes/time_tracking_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
context 'when both dates are added' do
it 'sets the correct note message' do
- expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and changed due date to #{due_date.to_s(:long)}")
+ expect(note.note).to eq("changed start date to #{start_date.to_fs(:long)} and changed due date to #{due_date.to_fs(:long)}")
end
end
@@ -37,7 +37,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
end
it 'sets the correct note message' do
- expect(note.note).to eq("removed start date #{start_date.to_s(:long)} and removed due date #{due_date.to_s(:long)}")
+ expect(note.note).to eq("removed start date #{start_date.to_fs(:long)} and removed due date #{due_date.to_fs(:long)}")
end
end
@@ -45,14 +45,14 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
let(:changed_dates) { { 'due_date' => [nil, due_date] } }
it 'sets the correct note message' do
- expect(note.note).to eq("changed due date to #{due_date.to_s(:long)}")
+ expect(note.note).to eq("changed due date to #{due_date.to_fs(:long)}")
end
context 'and start date removed' do
let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [start_date, nil] } }
it 'sets the correct note message' do
- expect(note.note).to eq("removed start date #{start_date.to_s(:long)} and changed due date to #{due_date.to_s(:long)}")
+ expect(note.note).to eq("removed start date #{start_date.to_fs(:long)} and changed due date to #{due_date.to_fs(:long)}")
end
end
end
@@ -73,14 +73,14 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
end
it 'sets the correct note message' do
- expect(note.note).to eq("changed start date to #{start_date.to_s(:long)}")
+ expect(note.note).to eq("changed start date to #{start_date.to_fs(:long)}")
end
context 'and due date removed' do
let(:changed_dates) { { 'due_date' => [due_date, nil], 'start_date' => [nil, start_date] } }
it 'sets the correct note message' do
- expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and removed due date #{due_date.to_s(:long)}")
+ expect(note.note).to eq("changed start date to #{start_date.to_fs(:long)} and removed due date #{due_date.to_fs(:long)}")
end
end
end
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 31f97edbd08..35c827d5448 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -207,5 +207,26 @@ RSpec.describe TestHooks::ProjectService, feature_category: :code_testing do
expect(service.execute).to include(success_result)
end
end
+
+ context 'emoji' do
+ let(:trigger) { 'emoji_events' }
+ let(:trigger_key) { :emoji_hooks }
+
+ it 'returns error message if not enough data' do
+ expect(hook).not_to receive(:execute)
+ expect(service.execute).to have_attributes(status: :error, message: 'Ensure the project has notes.')
+ end
+
+ it 'executes hook' do
+ note = create(:note)
+ allow(project).to receive_message_chain(:notes, :any?).and_return(true)
+ allow(project).to receive_message_chain(:notes, :last).and_return(note)
+ allow(Gitlab::DataBuilder::Emoji).to receive(:build).with(anything, current_user, 'award')
+ .and_return(sample_data)
+
+ expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
+ expect(service.execute).to include(success_result)
+ end
+ end
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 1ec6a3250fc..32e17df4d69 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -717,6 +717,57 @@ RSpec.describe TodoService, feature_category: :team_planning do
end
end
+ describe 'Work Items' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project, author: author) }
+
+ describe '#mark_todo' do
+ it 'creates a todo from a work item' do
+ service.mark_todo(work_item, author)
+
+ should_create_todo(user: author, target: work_item, action: Todo::MARKED)
+ end
+ end
+
+ describe '#todo_exists?' do
+ it 'returns false when no todo exist for the given work_item' do
+ expect(service.todo_exist?(work_item, author)).to be_falsy
+ end
+
+ it 'returns true when a todo exist for the given work_item' do
+ service.mark_todo(work_item, author)
+
+ expect(service.todo_exist?(work_item, author)).to be_truthy
+ end
+ end
+
+ describe '#resolve_todos_for_target' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: work_item, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: work_item, author: author)
+
+ service.resolve_todos_for_target(work_item, john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+
+ describe 'cached counts' do
+ it 'updates when todos change' do
+ create(:todo, :assigned, user: john_doe, project: project, target: work_item, author: author)
+
+ expect(john_doe.todos_done_count).to eq(0)
+ expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.resolve_todos_for_target(work_item, john_doe)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ end
+ end
+ end
+ end
+
describe '#reassigned_assignable' do
let(:described_method) { :reassigned_assignable }
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 563af8e7e9e..a50bd3ee2f1 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe UserProjectAccessChangedService, feature_category: :system_access
end
context 'with load balancing enabled' do
- let(:service) { UserProjectAccessChangedService.new([1, 2]) }
+ let(:service) { described_class.new([1, 2]) }
before do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
@@ -81,7 +81,7 @@ RSpec.describe UserProjectAccessChangedService, feature_category: :system_access
service.execute
end
- service = UserProjectAccessChangedService.new([1, 2, 3, 4, 5])
+ service = described_class.new([1, 2, 3, 4, 5])
allow(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2], [3], [4], [5]])
diff --git a/spec/services/users/allow_possible_spam_service_spec.rb b/spec/services/users/allow_possible_spam_service_spec.rb
new file mode 100644
index 00000000000..53618f0c8e9
--- /dev/null
+++ b/spec/services/users/allow_possible_spam_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::AllowPossibleSpamService, feature_category: :user_management do
+ let_it_be(:current_user) { create(:admin) }
+
+ subject(:service) { described_class.new(current_user) }
+
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ subject(:operation) { service.execute(user) }
+
+ it 'updates the custom attributes', :aggregate_failures do
+ expect(user.custom_attributes).to be_empty
+
+ operation
+ user.reload
+
+ expect(user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM)).to be_present
+ end
+ end
+end
diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb
index 5be5de82e91..7e342340f88 100644
--- a/spec/services/users/ban_service_spec.rb
+++ b/spec/services/users/ban_service_spec.rb
@@ -38,6 +38,13 @@ RSpec.describe Users::BanService, feature_category: :user_management do
ban_user
end
+
+ it 'tracks the event', :experiment do
+ expect(experiment(:phone_verification_for_low_risk_users))
+ .to track(:banned).on_next_instance.with_context(user: user)
+
+ ban_user
+ end
end
context 'when failed' do
diff --git a/spec/services/users/disallow_possible_spam_service_spec.rb b/spec/services/users/disallow_possible_spam_service_spec.rb
new file mode 100644
index 00000000000..32a47e05525
--- /dev/null
+++ b/spec/services/users/disallow_possible_spam_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::DisallowPossibleSpamService, feature_category: :user_management do
+ let_it_be(:current_user) { create(:admin) }
+
+ subject(:service) { described_class.new(current_user) }
+
+ describe '#execute' do
+ let(:user) { create(:user) }
+
+ subject(:operation) { service.execute(user) }
+
+ before do
+ UserCustomAttribute.upsert_custom_attributes(
+ [{
+ user_id: user.id,
+ key: :allow_possible_spam,
+ value: 'not important'
+ }]
+ )
+ end
+
+ it 'updates the custom attributes', :aggregate_failures do
+ expect(user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM)).to be_present
+
+ operation
+ user.reload
+
+ expect(user.custom_attributes).to be_empty
+ end
+ end
+end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 2aa62f932ed..fb7d487b29b 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -69,20 +69,23 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
end
describe '#execute' do
- let!(:uuid) { SecureRandom.uuid }
+ let(:uuid) { SecureRandom.uuid }
+ let!(:recursion_uuid) { SecureRandom.uuid }
let(:headers) do
{
'Content-Type' => 'application/json',
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
+ 'X-Gitlab-Webhook-UUID' => uuid,
'X-Gitlab-Event' => 'Push Hook',
- 'X-Gitlab-Event-UUID' => uuid,
+ 'X-Gitlab-Event-UUID' => recursion_uuid,
'X-Gitlab-Instance' => Gitlab.config.gitlab.base_url
}
end
before do
- # Set a stable value for the `X-Gitlab-Event-UUID` header.
- Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
+ # Set stable values for the `X-Gitlab-Webhook-UUID` and `X-Gitlab-Event-UUID` headers.
+ allow(SecureRandom).to receive(:uuid).and_return(uuid)
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(recursion_uuid)
end
context 'when there is an interpolation error' do
diff --git a/spec/services/webauthn/authenticate_service_spec.rb b/spec/services/webauthn/authenticate_service_spec.rb
index ca940dff0eb..99b8c7b0b36 100644
--- a/spec/services/webauthn/authenticate_service_spec.rb
+++ b/spec/services/webauthn/authenticate_service_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Webauthn::AuthenticateService, feature_category: :system_access d
get_result = client.get(challenge: challenge)
get_result['clientExtensionResults'] = {}
- service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge)
+ service = described_class.new(user, get_result.to_json, challenge)
expect(service.execute).to eq true
end
@@ -41,7 +41,7 @@ RSpec.describe Webauthn::AuthenticateService, feature_category: :system_access d
get_result = other_client.get(challenge: challenge)
get_result['clientExtensionResults'] = {}
- service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge)
+ service = described_class.new(user, get_result.to_json, challenge)
expect(service.execute).to eq false
end
@@ -49,7 +49,7 @@ RSpec.describe Webauthn::AuthenticateService, feature_category: :system_access d
context 'when device response includes invalid json' do
it 'returns false' do
- service = Webauthn::AuthenticateService.new(user, 'invalid JSON', '')
+ service = described_class.new(user, 'invalid JSON', '')
expect(service.execute).to eq false
end
end
diff --git a/spec/services/webauthn/register_service_spec.rb b/spec/services/webauthn/register_service_spec.rb
index 2286d261e94..734e8444b5d 100644
--- a/spec/services/webauthn/register_service_spec.rb
+++ b/spec/services/webauthn/register_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Webauthn::RegisterService, feature_category: :system_access do
webauthn_credential = WebAuthn::Credential.from_create(create_result)
params = { device_response: create_result.to_json, name: 'abc' }
- service = Webauthn::RegisterService.new(user, params, challenge)
+ service = described_class.new(user, params, challenge)
registration = service.execute
expect(registration.credential_xid).to eq(Base64.strict_encode64(webauthn_credential.raw_id))
@@ -27,7 +27,7 @@ RSpec.describe Webauthn::RegisterService, feature_category: :system_access do
create_result = client.create(challenge: Base64.strict_encode64(SecureRandom.random_bytes(16))) # rubocop:disable Rails/SaveBang
params = { device_response: create_result.to_json, name: 'abc' }
- service = Webauthn::RegisterService.new(user, params, challenge)
+ service = described_class.new(user, params, challenge)
registration = service.execute
expect(registration.errors.size).to eq(1)
diff --git a/spec/services/work_items/export_csv_service_spec.rb b/spec/services/work_items/export_csv_service_spec.rb
index 948ff89245e..4566289231f 100644
--- a/spec/services/work_items/export_csv_service_spec.rb
+++ b/spec/services/work_items/export_csv_service_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe WorkItems::ExportCsvService, :with_license, feature_category: :te
end
specify 'created_at' do
- expect(csv[0]['Created At (UTC)']).to eq(work_item_1.created_at.to_s(:csv))
+ expect(csv[0]['Created At (UTC)']).to eq(work_item_1.created_at.to_fs(:csv))
end
specify 'description' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 30c16458353..8e19650d980 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
context 'when work item type is not the default Issue' do
before do
task_type = WorkItems::Type.default_by_type(:task)
- work_item.update_columns(issue_type: task_type.base_type, work_item_type_id: task_type.id)
+ work_item.update_columns(work_item_type_id: task_type.id)
end
it 'does not apply the quick action' do
@@ -55,7 +55,7 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
end
context 'when work item type is the default Issue' do
- let(:issue) { create(:work_item, :issue, description: '') }
+ let(:issue) { create(:work_item, description: '') }
it 'applies the quick action' do
expect do
@@ -383,6 +383,38 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
end
end
end
+
+ context 'for current user todos widget' do
+ let_it_be(:user_todo) { create(:todo, target: work_item, user: developer, project: project, state: :pending) }
+ let_it_be(:other_todo) { create(:todo, target: work_item, user: create(:user), project: project, state: :pending) }
+
+ context 'when action is mark_as_done' do
+ let(:widget_params) { { current_user_todos_widget: { action: 'mark_as_done' } } }
+
+ it 'marks current user todo as done' do
+ expect do
+ update_work_item
+ user_todo.reload
+ other_todo.reload
+ end.to change(user_todo, :state).from('pending').to('done').and not_change { other_todo.state }
+ end
+
+ it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do
+ subject(:execute_service) { update_work_item }
+ end
+ end
+
+ context 'when action is add' do
+ let(:widget_params) { { current_user_todos_widget: { action: 'add' } } }
+
+ it 'adds a ToDo for the work item' do
+ expect do
+ update_work_item
+ work_item.reload
+ end.to change(Todo, :count).by(1)
+ end
+ end
+ end
end
describe 'label updates' do
diff --git a/spec/services/work_items/widgets/current_user_todos_service/update_service_spec.rb b/spec/services/work_items/widgets/current_user_todos_service/update_service_spec.rb
index 85b7e7a70df..aa7257e9e62 100644
--- a/spec/services/work_items/widgets/current_user_todos_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/current_user_todos_service/update_service_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe WorkItems::Widgets::CurrentUserTodosService::UpdateService, featu
todo = current_user.todos.last
- expect(todo.target).to eq(work_item)
+ expect(todo.target.id).to eq(work_item.id)
expect(todo).to be_pending
end
end
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index bea312369f7..47a2f43e30c 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -3,7 +3,7 @@
require 'simplecov'
require 'simplecov-cobertura'
require 'simplecov-lcov'
-require_relative '../lib/gitlab/utils'
+require 'gitlab/utils/all'
module SimpleCovEnv
extend self
@@ -53,31 +53,40 @@ module SimpleCovEnv
def configure_profile
SimpleCov.configure do
load_profile 'test_frameworks'
- track_files '{app,config/initializers,config/initializers_before_autoloader,db/post_migrate,haml_lint,lib,rubocop,tooling}/**/*.rb'
- add_filter '/vendor/ruby/'
- add_filter '/bin/'
- add_filter 'db/fixtures/development/' # Matches EE files as well
- add_filter %r|db/migrate/\d{14}_init_schema\.rb\z|
+ add_filter %r{^/(ee/)?(bin|gems|vendor)}
+ add_filter %r{^/(ee/)?db/fixtures/development}
+ add_filter %r{^/(ee/)?db/migrate/\d{14}_init_schema\.rb\z}
- add_group 'Channels', 'app/channels' # Matches EE files as well
- add_group 'Controllers', 'app/controllers' # Matches EE files as well
- add_group 'Finders', 'app/finders' # Matches EE files as well
- add_group 'GraphQL', 'app/graphql' # Matches EE files as well
- add_group 'Helpers', 'app/helpers' # Matches EE files as well
- add_group 'Mailers', 'app/mailers' # Matches EE files as well
- add_group 'Models', 'app/models' # Matches EE files as well
- add_group 'Policies', 'app/policies' # Matches EE files as well
- add_group 'Presenters', 'app/presenters' # Matches EE files as well
- add_group 'Serializers', 'app/serializers' # Matches EE files as well
- add_group 'Services', 'app/services' # Matches EE files as well
- add_group 'Uploaders', 'app/uploaders' # Matches EE files as well
- add_group 'Validators', 'app/validators' # Matches EE files as well
- add_group 'Workers', %w[app/jobs app/workers] # Matches EE files as well
- add_group 'Initializers', %w[config/initializers config/initializers_before_autoloader] # Matches EE files as well
- add_group 'Migrations', %w[db/migrate db/optional_migrations db/post_migrate] # Matches EE files as well
- add_group 'Libraries', %w[/lib /ee/lib]
- add_group 'Tooling', %w[/haml_lint /rubocop /tooling]
+ add_group 'Channels', %r{^/(ee/)?app/channels}
+ add_group 'Components', %r{^/(ee/)?app/components}
+ add_group 'Config', %r{^/(ee/)?config}
+ add_group 'Controllers', %r{^/(ee/)?app/controllers}
+ add_group 'Elastic migrations', %r{^/(ee/)?elastic}
+ add_group 'Enums', %r{^/(ee/)?app/enums}
+ add_group 'Events', %r{^/(ee/)?app/events}
+ add_group 'Experiments', %r{^/(ee/)?app/experiments}
+ add_group 'Finders', %r{^/(ee/)?app/finders}
+ add_group 'Fixtures', %r{^/(ee/)?db/fixtures}
+ add_group 'GraphQL', %r{^/(ee/)?app/graphql}
+ add_group 'Helpers', %r{^/(ee/)?app/helpers}
+ add_group 'Libraries', %r{^/(ee/)?lib}
+ add_group 'Mailers', %r{^/(ee/)?app/mailers}
+ add_group 'Metrics server', %r{^/(ee/)?metrics_server}
+ add_group 'Migrations', %r{^/(ee/)?db/(geo/)?(migrate|optional_migrations|post_migrate)}
+ add_group 'Models', %r{^/(ee/)?app/models}
+ add_group 'Policies', %r{^/(ee/)?app/policies}
+ add_group 'Presenters', %r{^/(ee/)?app/presenters}
+ add_group 'Replicators', %r{^/(ee/)?app/replicators}
+ add_group 'Seeds', %r{^/(ee/)?db/seeds}
+ add_group 'Serializers', %r{^/(ee/)?app/serializers}
+ add_group 'Services', %r{^/(ee/)?app/services}
+ add_group 'Sidekiq cluster', %r{^/(ee/)?sidekiq_cluster}
+ add_group 'Tooling', %r{^/(ee/)?(danger|haml_lint|rubocop|scripts|tooling)}
+ add_group 'Uploaders', %r{^/(ee/)?app/uploaders}
+ add_group 'Validators', %r{^/(ee/)?app/validators}
+ add_group 'Views', %r{^/(ee/)?app/views}
+ add_group 'Workers', %r{^/(ee/)?app/workers}
merge_timeout 365 * 24 * 3600
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index a2afa3d0ca7..4d66784d943 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -38,6 +38,8 @@ require 'test_prof/factory_prof/nate_heckler'
require 'parslet/rig/rspec'
require 'axe-rspec'
+require 'rspec_flaky'
+
rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
ENV['RSPEC_PROFILING']
@@ -100,6 +102,32 @@ RSpec.configure do |config|
end
end
+ config.after do |example|
+ # We fail early if we detect a PG::QueryCanceled error
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/402915
+ if example.exception && example.exception.message.include?('PG::QueryCanceled')
+ ENV['RSPEC_BYPASS_SYSTEM_EXIT_PROTECTION'] = 'true'
+
+ warn
+ warn "********************************************************************************************"
+ warn "********************************************************************************************"
+ warn "********************************************************************************************"
+ warn "* *"
+ warn "* We have detected a PG::QueryCanceled error in the specs, so we're failing early. *"
+ warn "* Please retry this job. *"
+ warn "* *"
+ warn "* See https://gitlab.com/gitlab-org/gitlab/-/issues/402915 for more info. *"
+ warn "* *"
+ warn "********************************************************************************************"
+ warn "********************************************************************************************"
+ warn "********************************************************************************************"
+ warn
+
+ exit 3
+ end
+ end
+
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata|
location = metadata[:location]
@@ -124,7 +152,7 @@ RSpec.configure do |config|
# Admin controller specs get auto admin mode enabled since they are
# protected by the 'EnforcesAdminAuthentication' concern
- metadata[:enable_admin_mode] = true if location =~ %r{(ee)?/spec/controllers/admin/}
+ metadata[:enable_admin_mode] = true if %r{(ee)?/spec/controllers/admin/}.match?(location)
end
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_docs\.rb\z}) do |metadata|
@@ -202,11 +230,7 @@ RSpec.configure do |config|
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
end
- require_relative '../tooling/rspec_flaky/config'
-
if RspecFlaky::Config.generate_report?
- require_relative '../tooling/rspec_flaky/listener'
-
config.reporter.register_listener(
RspecFlaky::Listener.new,
:example_passed,
@@ -312,6 +336,9 @@ RSpec.configure do |config|
# most cases. We do test the email verification flow in the appropriate specs.
stub_feature_flags(require_email_verification: false)
+ # Keep-around refs should only be turned off for specific projects/repositories.
+ stub_feature_flags(disable_keep_around_refs: false)
+
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
@@ -407,7 +434,7 @@ RSpec.configure do |config|
Gitlab::SidekiqMiddleware.server_configurator(
metrics: false, # The metrics don't go anywhere in tests
arguments_logger: false, # We're not logging the regular messages for inline jobs
- defer_jobs: false # We're not deferring jobs for inline tests
+ skip_jobs: false # We're not skipping jobs for inline tests
).call(chain)
chain.add DisableQueryLimit
chain.insert_after ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware, IsolatedRequestStore
diff --git a/spec/support/ability_check.rb b/spec/support/ability_check.rb
index 213944506bb..5b56b9925f6 100644
--- a/spec/support/ability_check.rb
+++ b/spec/support/ability_check.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'gitlab/utils/strong_memoize'
+require 'gitlab/utils/all'
module Support
module AbilityCheck
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index c7b2a03fde2..392743fda4a 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -5,6 +5,7 @@ require 'capybara/rails'
require 'capybara/rspec'
require 'capybara-screenshot/rspec'
require 'selenium-webdriver'
+require 'gitlab/utils/all'
# Give CI some extra time
timeout = ENV['CI'] || ENV['CI_SERVER'] ? 30 : 10
@@ -117,7 +118,7 @@ Capybara.register_driver :firefox do |app|
options.add_argument("--window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
# Run headless by default unless WEBDRIVER_HEADLESS specified
- options.add_argument("--headless") unless ENV['WEBDRIVER_HEADLESS'] =~ /^(false|no|0)$/i
+ options.add_argument("--headless") unless Gitlab::Utils.to_boolean(ENV['WEBDRIVER_HEADLESS'], default: false)
Capybara::Selenium::Driver.new(
app,
@@ -233,5 +234,6 @@ RSpec.configure do |config|
# We don't reset the session when the example failed, because we need capybara-screenshot to have access to it.
Capybara.reset_sessions! unless example.exception
block_and_wait_for_requests_complete
+ block_and_wait_for_action_cable_requests_complete
end
end
diff --git a/spec/support/database/click_house/hooks.rb b/spec/support/database/click_house/hooks.rb
new file mode 100644
index 00000000000..27abd19dc3f
--- /dev/null
+++ b/spec/support/database/click_house/hooks.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+# rubocop: disable Gitlab/NamespacedClass
+class ClickHouseTestRunner
+ def truncate_tables
+ ClickHouse::Client.configuration.databases.each_key do |db|
+ tables_for(db).each do |table|
+ ClickHouse::Client.execute("TRUNCATE TABLE #{table}", db)
+ end
+ end
+ end
+
+ def ensure_schema
+ return if @ensure_schema
+
+ ClickHouse::Client.configuration.databases.each_key do |db|
+ # drop all tables
+ lookup_tables(db).each do |table|
+ ClickHouse::Client.execute("DROP TABLE IF EXISTS #{table}", db)
+ end
+
+ # run the schema SQL files
+ Dir[Rails.root.join("db/click_house/#{db}/*.sql")].each do |file|
+ ClickHouse::Client.execute(File.read(file), db)
+ end
+ end
+
+ @ensure_schema = true
+ end
+
+ private
+
+ def tables_for(db)
+ @tables ||= {}
+ @tables[db] ||= lookup_tables(db)
+ end
+
+ def lookup_tables(db)
+ ClickHouse::Client.select('SHOW TABLES', db).pluck('name')
+ end
+end
+# rubocop: enable Gitlab/NamespacedClass
+
+RSpec.configure do |config|
+ click_house_test_runner = ClickHouseTestRunner.new
+
+ config.around(:each, :click_house) do |example|
+ with_net_connect_allowed do
+ click_house_test_runner.ensure_schema
+ click_house_test_runner.truncate_tables
+
+ example.run
+ end
+ end
+end
diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb
index 540c287bdad..443216ba9df 100644
--- a/spec/support/database/prevent_cross_joins.rb
+++ b/spec/support/database/prevent_cross_joins.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# This module tries to discover and prevent cross-joins across tables
-# This will forbid usage of tables between CI and main database
+# This will forbid usage of tables of different gitlab_schemas
# on a same query unless explicitly allowed by. This will change execution
# from a given point to allow cross-joins. The state will be cleared
# on a next test run.
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index b9a99eff413..17b4270fa20 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -99,7 +99,7 @@ module DbCleaner
AND pid <> pg_backend_pid();
SQL
- Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection|
+ Gitlab::Database::EachDatabase.each_connection(include_shared: false) do |connection|
connection.execute(cmd)
end
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index 7ac7e88867a..5de8e8cdca2 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -6,6 +6,7 @@
- Namespaces::BilledUsersFinder # Reason: There is no need to have anything else besides the ids is current structure
- Namespaces::FreeUserCap::UsersFinder # Reason: There is no need to have anything else besides the count
- Groups::EnvironmentScopesFinder # Reason: There is no need to have anything else besides the simple strucutre with the scope name
+- Security::RelatedPipelinesFinder # Reason: There is no need to have anything else besides the IDs of pipelines
# Temporary excludes (aka TODOs)
# For example:
diff --git a/spec/support/formatters/json_formatter.rb b/spec/support/formatters/json_formatter.rb
index 10af5445b7a..1fb0c7c91ec 100644
--- a/spec/support/formatters/json_formatter.rb
+++ b/spec/support/formatters/json_formatter.rb
@@ -26,11 +26,15 @@ module Support
hash[:exceptions] = exceptions.map do |exception|
hash = {
class: exception.class.name,
- message: exception.message,
- message_lines: strip_ansi_codes(notification.message_lines),
- backtrace: notification.formatted_backtrace
+ message: exception.message
}
+ hash[:backtrace] = notification.formatted_backtrace if notification.respond_to?(:formatted_backtrace)
+
+ if notification.respond_to?(:message_lines)
+ hash[:message_lines] = strip_ansi_codes(notification.message_lines)
+ end
+
if loglinking
hash.merge!(
correlation_id: exception.message[match_data_after(loglinking::CORRELATION_ID_TITLE)],
@@ -74,7 +78,8 @@ module Support
product_group: example.metadata[:product_group],
feature_category: example.metadata[:feature_category],
ci_job_url: ENV['CI_JOB_URL'],
- retry_attempts: example.metadata[:retry_attempts]
+ retry_attempts: example.metadata[:retry_attempts],
+ level: example.metadata[:level]
}
end
diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb
index 83c18f8073f..a6cc2560d0b 100644
--- a/spec/support/helpers/content_editor_helpers.rb
+++ b/spec/support/helpers/content_editor_helpers.rb
@@ -1,8 +1,16 @@
# frozen_string_literal: true
module ContentEditorHelpers
+ def close_rich_text_promo_popover_if_present
+ return unless page.has_css?("[data-testid='rich-text-promo-popover']")
+
+ page.within("[data-testid='rich-text-promo-popover']") do
+ click_button "Close"
+ end
+ end
+
def switch_to_content_editor
- click_button("Switch to rich text")
+ click_button("Switch to rich text editing")
end
def type_in_content_editor(keys)
diff --git a/spec/support/helpers/database/migration_testing_helpers.rb b/spec/support/helpers/database/migration_testing_helpers.rb
index 916446e66b7..87557860ab6 100644
--- a/spec/support/helpers/database/migration_testing_helpers.rb
+++ b/spec/support/helpers/database/migration_testing_helpers.rb
@@ -2,11 +2,13 @@
module Database
module MigrationTestingHelpers
- def define_background_migration(name)
- klass = Class.new do
+ def define_background_migration(name, with_base_class: false, scoping: nil)
+ klass = Class.new(with_base_class ? Gitlab::BackgroundMigration::BatchedMigrationJob : Object) do
# Can't simply def perform here as we won't have access to the block,
# similarly can't define_method(:perform, &block) here as it would change the block receiver
define_method(:perform) { |*args| yield(*args) }
+
+ scope_to(scoping) if scoping
end
stub_const("Gitlab::BackgroundMigration::#{name}", klass)
klass
diff --git a/spec/support/helpers/database/multiple_databases_helpers.rb b/spec/support/helpers/database/multiple_databases_helpers.rb
index fcdf820642d..bccd6979af1 100644
--- a/spec/support/helpers/database/multiple_databases_helpers.rb
+++ b/spec/support/helpers/database/multiple_databases_helpers.rb
@@ -19,7 +19,7 @@ module Database
def execute_on_each_database(query, databases: %I[main ci])
databases = databases.select { |database_name| database_exists?(database_name) }
- Gitlab::Database::EachDatabase.each_database_connection(only: databases, include_shared: false) do |connection, _|
+ Gitlab::Database::EachDatabase.each_connection(only: databases, include_shared: false) do |connection, _|
next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_shared)
connection.execute(query)
diff --git a/spec/support/helpers/emails_helper_test_helper.rb b/spec/support/helpers/emails_helper_test_helper.rb
index ea7dbc89ebd..572b2f6853d 100644
--- a/spec/support/helpers/emails_helper_test_helper.rb
+++ b/spec/support/helpers/emails_helper_test_helper.rb
@@ -2,7 +2,7 @@
module EmailsHelperTestHelper
def default_header_logo
- %r{<img alt="GitLab" src="/images/mailers/gitlab_logo\.(?:gif|png)" width="\d+" height="\d+" />}
+ %r{<img alt="GitLab" src="http://test.host/images/mailers/gitlab_logo\.(?:gif|png)" width="\d+" height="\d+" />}
end
end
diff --git a/spec/support/helpers/features/autocomplete_helpers.rb b/spec/support/helpers/features/autocomplete_helpers.rb
new file mode 100644
index 00000000000..106e823afef
--- /dev/null
+++ b/spec/support/helpers/features/autocomplete_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Features
+ module AutocompleteHelpers
+ def find_autocomplete_menu
+ find('.atwho-view ul', visible: true)
+ end
+
+ def find_highlighted_autocomplete_item
+ find('.atwho-view li.cur', visible: true)
+ end
+ end
+end
diff --git a/spec/support/helpers/features/invite_members_modal_helpers.rb b/spec/support/helpers/features/invite_members_modal_helpers.rb
index 75573616686..deb75cffe0d 100644
--- a/spec/support/helpers/features/invite_members_modal_helpers.rb
+++ b/spec/support/helpers/features/invite_members_modal_helpers.rb
@@ -52,7 +52,7 @@ module Features
click_on 'Select a group'
wait_for_requests
- click_button name
+ find('[role="option"]', text: name).click
choose_options(role, expires_at)
submit_invites
diff --git a/spec/support/helpers/features/iteration_helpers.rb b/spec/support/helpers/features/iteration_helpers.rb
index fab373a547f..7ae546fb83c 100644
--- a/spec/support/helpers/features/iteration_helpers.rb
+++ b/spec/support/helpers/features/iteration_helpers.rb
@@ -3,7 +3,7 @@
module Features
module IterationHelpers
def iteration_period(iteration)
- "#{iteration.start_date.to_s(:medium)} - #{iteration.due_date.to_s(:medium)}"
+ "#{iteration.start_date.to_fs(:medium)} - #{iteration.due_date.to_fs(:medium)}"
end
end
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 7db9e0aaf09..06390406efc 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -10,8 +10,7 @@ require 'securerandom'
require 'socket'
require 'logger'
require 'fileutils'
-
-require_relative '../../../lib/gitlab/utils'
+require 'gitlab/utils/all'
module GitalySetup
extend self
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index a9ad853b028..62e05129fb2 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -473,7 +473,7 @@ module GraphqlHelpers
end
def with_signature(variables, query)
- %Q[query(#{variables.map(&:sig).join(', ')}) #{wrap_query(query)}]
+ %[query(#{variables.map(&:sig).join(', ')}) #{wrap_query(query)}]
end
def var(type)
diff --git a/spec/support/helpers/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb
index 48b593fb3d1..84b06747f45 100644
--- a/spec/support/helpers/ldap_helpers.rb
+++ b/spec/support/helpers/ldap_helpers.rb
@@ -17,7 +17,7 @@ module LdapHelpers
# admin_group: 'my-admin-group'
# )
def stub_ldap_config(messages)
- allow_any_instance_of(::Gitlab::Auth::Ldap::Config).to receive_messages(messages)
+ allow_any_instance_of(::Gitlab::Auth::Ldap::Config).to receive_messages(to_settings(messages))
end
def stub_ldap_setting(messages)
diff --git a/spec/support/helpers/next_found_instance_of.rb b/spec/support/helpers/next_found_instance_of.rb
index c7079e64ffd..f53798c1856 100644
--- a/spec/support/helpers/next_found_instance_of.rb
+++ b/spec/support/helpers/next_found_instance_of.rb
@@ -2,7 +2,7 @@
module NextFoundInstanceOf
ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
- HELPER_METHOD_PATTERN = /(?:allow|expect)_next_found_(?<number>\d+)_instances_of/.freeze
+ HELPER_METHOD_PATTERN = /(?:allow|expect)_next_found_(?<number>\d+)_instances_of/
def method_missing(method_name, ...)
return super unless match_data = method_name.match(HELPER_METHOD_PATTERN)
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
index 3c88715615d..5cc63fe5c6e 100644
--- a/spec/support/helpers/next_instance_of.rb
+++ b/spec/support/helpers/next_instance_of.rb
@@ -31,8 +31,9 @@ module NextInstanceOf
receive_new.exactly(number).times
end
- target.to receive_new.and_wrap_original do |method, *original_args|
- method.call(*original_args).tap(&blk)
+ target.to receive_new.and_wrap_original do |*original_args, **original_kwargs|
+ method, *original_args = original_args
+ method.call(*original_args, **original_kwargs).tap(&blk)
end
end
end
diff --git a/spec/support/helpers/redis_helpers.rb b/spec/support/helpers/redis_helpers.rb
index 2c5ceb2f09e..b501ee79b26 100644
--- a/spec/support/helpers/redis_helpers.rb
+++ b/spec/support/helpers/redis_helpers.rb
@@ -6,4 +6,22 @@ module RedisHelpers
instance_class.with(&:flushdb)
end
end
+
+ # Defines a class of wrapper that uses `resque.yml` regardless of `config/redis.yml.example`
+ # this allows us to test against a standalone Redis even if Cache and SharedState are using
+ # Redis Cluster. We do not use queue as it does not perform redis cluster validations.
+ def define_helper_redis_store_class(store_name = "Sessions")
+ Class.new(Gitlab::Redis::Wrapper) do
+ define_singleton_method(:name) { store_name }
+
+ def config_file_name
+ config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
+ Rails.root.join(config_file_name).to_s
+ end
+ end
+ end
+
+ def create_redis_store(options, extras = {})
+ ::Redis::Store.new(options.merge(extras))
+ end
end
diff --git a/spec/support/helpers/reload_helpers.rb b/spec/support/helpers/reload_helpers.rb
index 71becd535b0..6b120c61ff2 100644
--- a/spec/support/helpers/reload_helpers.rb
+++ b/spec/support/helpers/reload_helpers.rb
@@ -4,9 +4,4 @@ module ReloadHelpers
def reload_models(*models)
models.compact.map(&:reload)
end
-
- def subject_and_reload(...)
- subject
- reload_models(...)
- end
end
diff --git a/spec/support/helpers/require_migration.rb b/spec/support/helpers/require_migration.rb
index ee28f8e504c..c9cb7b4d90c 100644
--- a/spec/support/helpers/require_migration.rb
+++ b/spec/support/helpers/require_migration.rb
@@ -15,7 +15,7 @@ class RequireMigration
end
MIGRATION_FOLDERS = %w[db/migrate db/post_migrate].freeze
- SPEC_FILE_PATTERN = %r{.+/(?:\d+_)?(?<file_name>.+)_spec\.rb}.freeze
+ SPEC_FILE_PATTERN = %r{.+/(?:\d+_)?(?<file_name>.+)_spec\.rb}
class << self
def require_migration!(file_name)
diff --git a/spec/support/helpers/stub_env.rb b/spec/support/helpers/stub_env.rb
deleted file mode 100644
index afa501d6279..00000000000
--- a/spec/support/helpers/stub_env.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
-module StubENV
- # Stub ENV variables
- #
- # You can provide either a key and value as separate params or both in a Hash format
- #
- # Keys and values will always be converted to String, to comply with how ENV behaves
- #
- # @param key_or_hash [String, Hash<String,String>]
- # @param value [String]
- def stub_env(key_or_hash, value = nil)
- init_stub unless env_stubbed?
-
- if key_or_hash.is_a? Hash
- key_or_hash.each do |key, value|
- add_stubbed_value(key, ensure_env_type(value))
- end
- else
- add_stubbed_value key_or_hash, ensure_env_type(value)
- end
- end
-
- private
-
- STUBBED_KEY = '__STUBBED__'
-
- def add_stubbed_value(key, value)
- allow(ENV).to receive(:[]).with(key).and_return(value)
- allow(ENV).to receive(:key?).with(key).and_return(true)
- allow(ENV).to receive(:fetch).with(key).and_return(value)
- allow(ENV).to receive(:fetch).with(key, anything) do |_, default_val|
- value || default_val
- end
- end
-
- def env_stubbed?
- ENV[STUBBED_KEY]
- end
-
- def init_stub
- allow(ENV).to receive(:[]).and_call_original
- allow(ENV).to receive(:key?).and_call_original
- allow(ENV).to receive(:fetch).and_call_original
- add_stubbed_value(STUBBED_KEY, true)
- end
-
- def ensure_env_type(value)
- value.nil? ? value : value.to_s
- end
-end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index da4954c1a5f..b95adb3fe4d 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -523,7 +523,7 @@ module TestEnv
def component_matches_git_sha?(component_folder, expected_version)
# Not a git SHA, so return early
- return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID
+ return false unless ::Gitlab::Git::COMMIT_ID.match?(expected_version)
return false unless Dir.exist?(component_folder)
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 73f7a79dd5b..8ac3b0c134b 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -50,8 +50,6 @@ module UsageDataHelpers
projects_asana_active
projects_jenkins_active
projects_jira_active
- projects_jira_dvcs_cloud_active
- projects_jira_dvcs_server_active
projects_slack_active
projects_slack_slash_commands_active
projects_custom_issue_tracker_active
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index 5e2e8ad53e0..b7a3b77a694 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -27,6 +27,17 @@ module WaitForRequests
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
+ def block_and_wait_for_action_cable_requests_complete
+ block_action_cable_requests { wait_for_action_cable_requests }
+ end
+
+ def block_action_cable_requests
+ Gitlab::Testing::ActionCableBlocker.block_requests!
+ yield
+ ensure
+ Gitlab::Testing::ActionCableBlocker.allow_requests!
+ end
+
# Wait for client-side AJAX requests
def wait_for_requests
wait_for('JS requests complete', max_wait_time: 2 * Capybara.default_max_wait_time) do
@@ -42,6 +53,12 @@ module WaitForRequests
end
end
+ def wait_for_action_cable_requests
+ wait_for('Action Cable requests complete') do
+ Gitlab::Testing::ActionCableBlocker.num_active_requests == 0
+ end
+ end
+
private
def finished_all_rack_requests?
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index b731ee626c0..28797229661 100644
--- a/spec/support/import_export/configuration_helper.rb
+++ b/spec/support/import_export/configuration_helper.rb
@@ -4,7 +4,7 @@ module ConfigurationHelper
# Returns a list of models from hashes/arrays contained in +project_tree+
def names_from_tree(project_tree)
project_tree.map do |branch_or_model|
- branch_or_model = branch_or_model.to_s if branch_or_model.is_a?(Symbol)
+ branch_or_model = branch_or_model.to_s if branch_or_model.is_a?(Symbol)
branch_or_model.is_a?(String) ? branch_or_model : names_from_tree(branch_or_model)
end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index ee1b4a3c33a..3be2d39906d 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -92,7 +92,7 @@ module ExportFileHelper
end
# Returns the offended ObjectWithParent object if a sensitive word is found inside a hash,
- # excluding the whitelisted safe hashes.
+ # excluding the allowlisted safe hashes.
def find_sensitive_attributes(sensitive_word, project_hash)
loop do
object_with_parent = deep_find_with_parent(sensitive_word, project_hash)
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index 4b08c13945c..29ebe5a3918 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -63,7 +63,7 @@ module ExceedQueryLimitHelpers
end
end
- MARGINALIA_ANNOTATION_REGEX = %r{\s*/\*.*\*/}.freeze
+ MARGINALIA_ANNOTATION_REGEX = %r{\s*/\*.*\*/}
DB_QUERY_RE = Regexp.union(
[
diff --git a/spec/support/matchers/have_native_text_validation_message.rb b/spec/support/matchers/have_native_text_validation_message.rb
new file mode 100644
index 00000000000..3923086dc70
--- /dev/null
+++ b/spec/support/matchers/have_native_text_validation_message.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_native_text_validation_message do |field|
+ match do |page|
+ message = page.find_field(field).native.attribute('validationMessage')
+ expect(message).to match(/Please fill [a-z]+ this field./)
+ end
+end
diff --git a/spec/support/matchers/result_matchers.rb b/spec/support/matchers/result_matchers.rb
new file mode 100644
index 00000000000..4fc2c06ba69
--- /dev/null
+++ b/spec/support/matchers/result_matchers.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# Example usage:
+#
+# expect(Result.ok(1)).to be_ok_result(1)
+#
+# expect(Result.err('hello')).to be_err_result do |result_value|
+# expect(result_value).to match(/hello/i)
+# end
+#
+# Argument to matcher is the expected value to be matched via '=='.
+# For more complex matching, pass a block to the matcher which will receive the result value as an argument.
+
+module ResultMatchers
+ def be_ok_result(expected_value = nil)
+ BeResult.new(ok_or_err: :ok, expected_value: expected_value)
+ end
+
+ def be_err_result(expected_value = nil)
+ BeResult.new(ok_or_err: :err, expected_value: expected_value)
+ end
+
+ class BeResult
+ attr_reader :ok_or_err, :actual, :failure_message_suffix, :expected_value
+
+ def initialize(ok_or_err:, expected_value:)
+ @ok_or_err = ok_or_err
+ @expected_value = expected_value
+ end
+
+ def matches?(actual, &block)
+ @actual = actual
+
+ raise "#{actual} must be a #{::Result}, but it was a #{actual.class}" unless actual.is_a?(::Result)
+
+ @failure_message_suffix = "be an '#{ok_or_err}' type"
+ return false unless actual.ok? == ok?
+
+ actual_value = actual.ok? ? actual.unwrap : actual.unwrap_err
+
+ if expected_value
+ @failure_message_suffix =
+ "have a value of #{expected_value.inspect}, but it was #{actual_value.inspect}"
+ return false unless actual_value == expected_value
+ end
+
+ # NOTE: A block can be passed to the matcher to perform more sophisticated matching,
+ # or to provide more concise and specific failure messages.
+ block ? block.yield(actual_value) : true
+ end
+
+ def failure_message
+ "expected #{actual.inspect} to #{failure_message_suffix}"
+ end
+
+ def failure_message_when_negated
+ "expected #{actual.inspect} not to #{failure_message_suffix}"
+ end
+
+ private
+
+ def ok?
+ ok_or_err == :ok
+ end
+ end
+end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 94c43669173..4479e679d67 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -5,10 +5,10 @@ require_relative "system_exit_detected"
require_relative "helpers/stub_configuration"
require_relative "helpers/stub_metrics"
require_relative "helpers/stub_object_storage"
-require_relative "helpers/stub_env"
require_relative "helpers/fast_rails_root"
-require_relative "../../lib/gitlab/utils"
+require 'gitlab/rspec/all'
+require 'gitlab/utils/all'
RSpec::Expectations.configuration.on_potential_false_positives = :raise
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 4168820a2b3..3cce22c00e6 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -166,7 +166,6 @@
- './ee/spec/controllers/security/vulnerabilities_controller_spec.rb'
- './ee/spec/controllers/sitemap_controller_spec.rb'
- './ee/spec/controllers/subscriptions_controller_spec.rb'
-- './ee/spec/controllers/subscriptions/groups_controller_spec.rb'
- './ee/spec/controllers/trial_registrations_controller_spec.rb'
- './ee/spec/controllers/users_controller_spec.rb'
- './ee/spec/db/production/license_spec.rb'
@@ -1213,8 +1212,6 @@
- './ee/spec/lib/elastic/latest/merge_request_config_spec.rb'
- './ee/spec/lib/elastic/latest/note_config_spec.rb'
- './ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb'
-- './ee/spec/lib/elastic/latest/project_wiki_class_proxy_spec.rb'
-- './ee/spec/lib/elastic/latest/project_wiki_instance_proxy_spec.rb'
- './ee/spec/lib/elastic/latest/routing_spec.rb'
- './ee/spec/lib/elastic/latest/snippet_instance_proxy_spec.rb'
- './ee/spec/lib/elastic/migration_spec.rb'
@@ -1523,8 +1520,6 @@
- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_groups_with_event_streaming_destinations_metric_spec.rb'
- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_external_status_checks_metric_spec.rb'
- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_saml_group_links_metric_spec.rb'
-- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_gbp_metric_spec.rb'
-- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_slack_app_installations_metric_spec.rb'
- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_users_associating_group_milestones_to_releases_metric_spec.rb'
- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_ci_builds_metric_spec.rb'
- './ee/spec/lib/gitlab/usage/metrics/instrumentations/count_users_deployment_approvals_spec.rb'
@@ -3538,8 +3533,6 @@
- './spec/db/schema_spec.rb'
- './spec/dependencies/omniauth_saml_spec.rb'
- './spec/experiments/application_experiment_spec.rb'
-- './spec/experiments/concerns/project_commit_count_spec.rb'
-- './spec/experiments/force_company_trial_experiment_spec.rb'
- './spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb'
- './spec/experiments/ios_specific_templates_experiment_spec.rb'
- './spec/features/abuse_report_spec.rb'
@@ -4151,7 +4144,6 @@
- './spec/features/projects/settings/registry_settings_spec.rb'
- './spec/features/projects/settings/repository_settings_spec.rb'
- './spec/features/projects/settings/secure_files_spec.rb'
-- './spec/features/projects/settings/service_desk_setting_spec.rb'
- './spec/features/projects/settings/user_archives_project_spec.rb'
- './spec/features/projects/settings/user_changes_avatar_spec.rb'
- './spec/features/projects/settings/user_changes_default_branch_spec.rb'
@@ -5251,7 +5243,6 @@
- './spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb'
-- './spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb'
- './spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb'
@@ -6492,7 +6483,6 @@
- './spec/lib/gitlab/graphql/batch_key_spec.rb'
- './spec/lib/gitlab/graphql/calls_gitaly/field_extension_spec.rb'
- './spec/lib/gitlab/graphql/copy_field_description_spec.rb'
-- './spec/lib/gitlab/graphql/generic_tracing_spec.rb'
- './spec/lib/gitlab/graphql/known_operations_spec.rb'
- './spec/lib/gitlab/graphql/lazy_spec.rb'
- './spec/lib/gitlab/graphql/loaders/batch_commit_loader_spec.rb'
@@ -9064,7 +9054,6 @@
- './spec/services/clusters/create_service_spec.rb'
- './spec/services/clusters/destroy_service_spec.rb'
- './spec/services/clusters/integrations/create_service_spec.rb'
-- './spec/services/clusters/integrations/prometheus_health_check_service_spec.rb'
- './spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb'
- './spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb'
- './spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb'
@@ -9126,10 +9115,6 @@
- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
- './spec/services/environments/stop_service_spec.rb'
- './spec/services/error_tracking/base_service_spec.rb'
-- './spec/services/error_tracking/issue_details_service_spec.rb'
-- './spec/services/error_tracking/issue_latest_event_service_spec.rb'
-- './spec/services/error_tracking/issue_update_service_spec.rb'
-- './spec/services/error_tracking/list_issues_service_spec.rb'
- './spec/services/error_tracking/list_projects_service_spec.rb'
- './spec/services/event_create_service_spec.rb'
- './spec/services/events/destroy_service_spec.rb'
@@ -9694,7 +9679,6 @@
- './spec/support_specs/helpers/stub_method_calls_spec.rb'
- './spec/support_specs/matchers/be_sorted_spec.rb'
- './spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
-- './spec/support_specs/time_travel_spec.rb'
- './spec/tasks/admin_mode_spec.rb'
- './spec/tasks/cache/clear/redis_spec.rb'
- './spec/tasks/config_lint_spec.rb'
@@ -9763,12 +9747,6 @@
- './spec/tooling/lib/tooling/test_map_packer_spec.rb'
- './spec/tooling/merge_request_spec.rb'
- './spec/tooling/quality/test_level_spec.rb'
-- './spec/tooling/rspec_flaky/config_spec.rb'
-- './spec/tooling/rspec_flaky/example_spec.rb'
-- './spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb'
-- './spec/tooling/rspec_flaky/flaky_example_spec.rb'
-- './spec/tooling/rspec_flaky/listener_spec.rb'
-- './spec/tooling/rspec_flaky/report_spec.rb'
- './spec/uploaders/attachment_uploader_spec.rb'
- './spec/uploaders/avatar_uploader_spec.rb'
- './spec/uploaders/ci/pipeline_artifact_uploader_spec.rb'
@@ -9872,7 +9850,6 @@
- './spec/views/layouts/profile.html.haml_spec.rb'
- './spec/views/layouts/_published_experiments.html.haml_spec.rb'
- './spec/views/layouts/signup_onboarding.html.haml_spec.rb'
-- './spec/views/layouts/simple_registration.html.haml_spec.rb'
- './spec/views/layouts/terms.html.haml_spec.rb'
- './spec/views/notify/autodevops_disabled_email.text.erb_spec.rb'
- './spec/views/notify/changed_milestone_email.html.haml_spec.rb'
diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb
index 12d4af5170b..8b4c1c1e243 100644
--- a/spec/support/shared_contexts/email_shared_context.rb
+++ b/spec/support/shared_contexts/email_shared_context.rb
@@ -24,7 +24,10 @@ end
def service_desk_fixture(path, slug: nil, key: 'mykey')
slug ||= project.full_path_slug.to_s
- fixture_file(path).gsub('project_slug', slug).gsub('project_key', key)
+ fixture_file(path)
+ .gsub('project_slug', slug)
+ .gsub('project_key', key)
+ .gsub('project_id', project.project_id.to_s)
end
RSpec.shared_examples 'reply processing shared examples' 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 21d9dccbb8d..8c17136b1e2 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -56,7 +56,7 @@ RSpec.shared_context 'with integration' do
hash.merge!(k => 'key:value')
elsif integration == 'packagist' && k == :server
hash.merge!(k => 'https://packagist.example.com')
- elsif k =~ /^(.*_url|url|webhook)/
+ elsif /^(.*_url|url|webhook)/.match?(k)
hash.merge!(k => "http://example.com")
elsif integration_klass.method_defined?("#{k}?")
hash.merge!(k => true)
diff --git a/spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb b/spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb
deleted file mode 100644
index a2cb9d41f45..00000000000
--- a/spec/support/shared_contexts/finders/packages/npm/package_finder_shared_context.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_context 'last_of_each_version setup context' do
- let_it_be(:package1) { create(:npm_package, name: 'test', version: '1.2.3', project: project) }
- let_it_be(:package2) { create(:npm_package, name: 'test2', version: '1.2.3', project: project) }
-
- let(:package_name) { 'test' }
- let(:version) { '1.2.3' }
-
- before do
- # create a duplicated package without triggering model validation errors
- package2.update_column(:name, 'test')
- end
-end
diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb
index 1b50ef3fcff..69c20a00c5a 100644
--- a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb
@@ -4,10 +4,11 @@ RSpec.shared_context 'structured_logger' do
let(:timestamp) { Time.iso8601('2018-01-01T12:00:00.000Z') }
let(:created_at) { timestamp - 1.second }
let(:scheduling_latency_s) { 1.0 }
+ let(:worker_class) { "TestWorker" }
let(:job) do
{
- "class" => "TestWorker",
+ "class" => worker_class,
"args" => [1234, 'hello', { 'key' => 'value' }],
"retry" => false,
"queue" => "cronjob:test_queue",
@@ -31,7 +32,7 @@ RSpec.shared_context 'structured_logger' do
job.except(
'exception.backtrace', 'exception.class', 'exception.message'
).merge(
- 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start',
+ 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: start",
'job_status' => 'start',
'pid' => Process.pid,
'created_at' => created_at.to_f,
@@ -55,7 +56,7 @@ RSpec.shared_context 'structured_logger' do
let(:end_payload) do
start_payload.merge(db_payload_defaults).merge(
- 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
+ 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: done: 0.0 sec",
'job_status' => 'done',
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
@@ -67,15 +68,23 @@ RSpec.shared_context 'structured_logger' do
let(:deferred_payload) do
end_payload.merge(
- 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: deferred: 0.0 sec',
+ 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: deferred: 0.0 sec",
'job_status' => 'deferred',
- 'job_deferred_by' => :feature_flag
+ 'job_deferred_by' => :feature_flag,
+ 'deferred_count' => 1
+ )
+ end
+
+ let(:dropped_payload) do
+ end_payload.merge(
+ 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: dropped: 0.0 sec',
+ 'job_status' => 'dropped'
)
end
let(:exception_payload) do
end_payload.merge(
- 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
+ 'message' => "#{worker_class} JID-da883554ee4fe414012f5f42: fail: 0.0 sec",
'job_status' => 'fail',
'exception.class' => 'ArgumentError',
'exception.message' => 'Something went wrong',
diff --git a/spec/support/shared_contexts/merge_request_create_shared_context.rb b/spec/support/shared_contexts/merge_request_create_shared_context.rb
index fc9a3767365..bf8eeeb7ab6 100644
--- a/spec/support/shared_contexts/merge_request_create_shared_context.rb
+++ b/spec/support/shared_contexts/merge_request_create_shared_context.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'merge request create context' do
+ include ContentEditorHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:target_project) { create(:project, :public, :repository) }
@@ -23,5 +25,7 @@ RSpec.shared_context 'merge request create context' do
source_branch: 'fix',
target_branch: 'master'
})
+
+ close_rich_text_promo_popover_if_present
end
end
diff --git a/spec/support/shared_contexts/merge_request_edit_shared_context.rb b/spec/support/shared_contexts/merge_request_edit_shared_context.rb
index f0e89b0c5f9..8fe0174b13e 100644
--- a/spec/support/shared_contexts/merge_request_edit_shared_context.rb
+++ b/spec/support/shared_contexts/merge_request_edit_shared_context.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'merge request edit context' do
+ include ContentEditorHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:milestone) { create(:milestone, project: target_project) }
@@ -25,5 +27,6 @@ RSpec.shared_context 'merge request edit context' do
sign_in(user)
visit edit_project_merge_request_path(target_project, merge_request)
+ close_rich_text_promo_popover_if_present
end
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index efb4d244c10..0abf688566a 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -82,6 +82,7 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Monitor'),
nav_sub_items: [
+ _('Tracing'),
_('Error Tracking'),
_('Alerts'),
_('Incidents')
diff --git a/spec/support/shared_contexts/prometheus/alert_shared_context.rb b/spec/support/shared_contexts/prometheus/alert_shared_context.rb
index 932ab899270..13e739680c8 100644
--- a/spec/support/shared_contexts/prometheus/alert_shared_context.rb
+++ b/spec/support/shared_contexts/prometheus/alert_shared_context.rb
@@ -37,17 +37,6 @@ RSpec.shared_context 'self-managed prometheus alert attributes' do
}
}
end
-
- let(:dashboard_url_for_alert) do
- Gitlab::Routing.url_helpers.metrics_dashboard_project_environment_url(
- project,
- environment,
- embed_json: embed_content,
- embedded: true,
- end: '2018-03-12T09:36:00Z',
- start: '2018-03-12T08:36:00Z'
- )
- end
end
RSpec.shared_context 'gitlab-managed prometheus alert attributes' do
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_metadata_shared_examples.rb b/spec/support/shared_contexts/requests/api/npm_packages_metadata_shared_examples.rb
new file mode 100644
index 00000000000..7faf7cb32ba
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/npm_packages_metadata_shared_examples.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'generates metadata response "on-the-fly"' do
+ let(:metadata) do
+ {
+ 'dist-tags' => {
+ 'latest' => package.version
+ },
+ 'name' => package.name,
+ 'versions' => {
+ package.version => {
+ 'dist' => {
+ 'shasum' => 'be93151dc23ac34a82752444556fe79b32c7a1ad',
+ 'tarball' => "http://localhost/api/v4/projects/#{project.id}/packages/npm/#{package.name}/-/foo-1.0.1.tgz"
+ },
+ 'name' => package.name,
+ 'version' => package.version
+ }
+ }
+ }
+ end
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ expect(endpoint).not_to receive(:present_carrierwave_file!) # rubocop:disable RSpec/ExpectInHook
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'generates metadata response "on-the-fly"', :aggregate_failures do
+ expect(Packages::Npm::GenerateMetadataService).to receive(:new).and_call_original
+
+ subject
+
+ expect(json_response).to eq(metadata)
+ end
+end
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index 1e50505162d..36103b94542 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -8,7 +8,6 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:group) { create(:group, name: 'test-group') }
let_it_be(:namespace) { group }
let_it_be(:project, reload: true) { create(:project, :public, namespace: namespace) }
- let_it_be(:package1, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.4') }
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.3') }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
@@ -18,11 +17,6 @@ RSpec.shared_context 'npm api setup' do
let(:package_name) { package.name }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_npm_user' } }
-
- before do
- # create a duplicated package without triggering model validation errors
- package1.update_column(:version, '1.2.3')
- end
end
RSpec.shared_context 'set package name from package name type' do
diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
index b34d95519a2..24f0d22da47 100644
--- a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
+++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
@@ -8,7 +8,7 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do
let(:standard_metrics) do
[
metric_attributes('recorded_at', 'standard'),
- metric_attributes('settings.collected_data_categories', 'standard', 'object')
+ metric_attributes('settings.collected_data_categories', 'standard', 'object', 'CollectedDataCategoriesMetric')
]
end
diff --git a/spec/support/shared_contexts/user_contribution_events_shared_context.rb b/spec/support/shared_contexts/user_contribution_events_shared_context.rb
index 681c2f0d811..48f0ac1e4ac 100644
--- a/spec/support/shared_contexts/user_contribution_events_shared_context.rb
+++ b/spec/support/shared_contexts/user_contribution_events_shared_context.rb
@@ -95,27 +95,52 @@ RSpec.shared_context 'with user contribution events' do
end
# pushed
- let_it_be(:push_event_payload_pushed) do
+ commit_title = 'Initial commit'
+ let_it_be(:push_event_branch_payload_pushed) do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event)
+ create(:push_event_payload, event: event, commit_title: commit_title)
event
end
- let_it_be(:push_event_payload_created) do
+ let_it_be(:push_event_branch_payload_created) do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event, action: :created)
+ create(:push_event_payload, event: event, action: :created, commit_title: commit_title)
event
end
- let_it_be(:push_event_payload_removed) do
+ let_it_be(:push_event_branch_payload_removed) do
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event, action: :removed)
event
end
+ let_it_be(:push_event_tag_payload_pushed) do
+ event = create(:push_event, project: project, author: user)
+ create(:push_event_payload, event: event, ref_type: :tag, commit_title: commit_title)
+ event
+ end
+
+ let_it_be(:push_event_tag_payload_created) do
+ event = create(:push_event, project: project, author: user)
+ create(:push_event_payload, event: event, ref_type: :tag, action: :created, commit_title: commit_title)
+ event
+ end
+
+ let_it_be(:push_event_tag_payload_removed) do
+ event = create(:push_event, project: project, author: user)
+ create(:push_event_payload, event: event, ref_type: :tag, action: :removed)
+ event
+ end
+
let_it_be(:bulk_push_event) do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event, commit_count: 5, commit_from: '83c6aa31482b9076531ed3a880e75627fd6b335c')
+ create(
+ :push_event_payload,
+ event: event,
+ commit_count: 5,
+ commit_from: '83c6aa31482b9076531ed3a880e75627fd6b335c',
+ commit_title: commit_title
+ )
event
end
diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb
index a2849e00d27..cdb1058e584 100644
--- a/spec/support/shared_examples/ci/stage_shared_examples.rb
+++ b/spec/support/shared_examples/ci/stage_shared_examples.rb
@@ -21,7 +21,7 @@ RSpec.shared_examples 'manual playable stage' do |stage_type|
context 'when is skipped' do
let(:status) { 'skipped' }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_falsy }
end
end
end
diff --git a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
index c8eaef764af..e61c884cd2b 100644
--- a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
+++ b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
@@ -113,7 +113,8 @@ RSpec.shared_examples 'every metric definition' do
Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric,
Gitlab::Usage::Metrics::Instrumentations::RedisMetric,
Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric,
- Gitlab::Usage::Metrics::Instrumentations::NumbersMetric
+ Gitlab::Usage::Metrics::Instrumentations::NumbersMetric,
+ Gitlab::Usage::Metrics::Instrumentations::PrometheusMetric
]
end
diff --git a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
new file mode 100644
index 00000000000..e2a4fb31361
--- /dev/null
+++ b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# - subject
+# - action
+# - user
+# Optionally, the context can contain:
+# - project
+# - namespace
+
+RSpec.shared_examples 'internal event tracking' do
+ let(:fake_tracker) { instance_spy(Gitlab::Tracking::Destinations::Snowplow) }
+ let(:namespace) { nil }
+ let(:proejct) { nil }
+
+ before do
+ allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_tracker)
+
+ allow(Gitlab::Tracking::StandardContext).to receive(:new).and_call_original
+ allow(Gitlab::Tracking::ServicePingContext).to receive(:new).and_call_original
+ end
+
+ it 'logs to Snowplow', :aggregate_failures do
+ subject
+
+ expect(Gitlab::Tracking::StandardContext)
+ .to have_received(:new)
+ .with(
+ project_id: project&.id,
+ user_id: user.id,
+ namespace_id: namespace&.id,
+ plan_name: namespace&.actual_plan_name
+ )
+
+ expect(Gitlab::Tracking::ServicePingContext)
+ .to have_received(:new)
+ .with(data_source: :redis_hll, event: action)
+
+ expect(fake_tracker).to have_received(:event)
+ .with(
+ 'InternalEventTracking',
+ action,
+ context: [
+ an_instance_of(SnowplowTracker::SelfDescribingJson),
+ an_instance_of(SnowplowTracker::SelfDescribingJson)
+ ]
+ )
+ .exactly(:once)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
deleted file mode 100644
index 5b63ef10c85..00000000000
--- a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples_for 'GET #metrics_dashboard correctly formatted response' do
- it 'returns a json object with the correct keys' do
- get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
-
- # Exclude `all_dashboards` to handle separately, at spec/controllers/projects/environments_controller_spec.rb:565
- # because `all_dashboards` key is not part of expected shared behavior
- found_keys = json_response.keys - ['all_dashboards']
-
- expect(response).to have_gitlab_http_status(status_code)
- expect(found_keys).to contain_exactly(*expected_keys)
- end
-end
-
-RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_name|
- let(:expected_keys) { %w(dashboard status metrics_data) }
- let(:status_code) { :ok }
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- end
-
- it_behaves_like 'GET #metrics_dashboard correctly formatted response'
-
- it 'returns correct dashboard' do
- get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
-
- expect(json_response['dashboard']['dashboard']).to eq(dashboard_name)
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it 'returns 404 not found' do
- get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to be_empty
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb
index 9cf35325202..ba3b08751da 100644
--- a/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb
@@ -75,7 +75,7 @@ RSpec.shared_examples 'a controller that can serve LFS files' do |options = {}|
file_uri = URI.parse(response.location)
params = CGI.parse(file_uri.query)
- expect(params["response-content-disposition"].first).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ expect(params["response-content-disposition"].first).to eq(%(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
end
diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
index ba00e3e0610..3d3b619451d 100644
--- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
@@ -6,7 +6,7 @@
# - category
# - action
# - namespace
-# Optionaly, the context can contain:
+# Optionally, the context can contain:
# - project
# - property
# - user
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 0792ac14e47..772e03950da 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -94,14 +94,6 @@ RSpec.shared_examples 'handle uploads' do
expect(response).to have_gitlab_http_status(:not_found)
end
-
- it 'is a working exploit without the validation' do
- allow_any_instance_of(FileUploader).to receive(:secret) { secret }
-
- show_upload
-
- expect(response).to have_gitlab_http_status(:ok)
- end
end
context 'when accessing a specific upload via different model' do
diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
index cb80751ff49..2bda352c11f 100644
--- a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
+++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
@@ -24,14 +24,6 @@ RSpec.shared_examples 'a cascading setting' do
include_examples 'subgroup settings are disabled'
- context 'when use_traversal_ids_for_ancestors is disabled' do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestors: false)
- end
-
- include_examples 'subgroup settings are disabled'
- end
-
it 'does not show enforcement checkbox in subgroups' do
visit subgroup_path
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 f70288168d7..254bc3c83ac 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -506,6 +506,8 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
switch_to_content_editor
type_in_content_editor :enter
+
+ stub_feature_flags(disable_all_mention: false)
end
if params[:with_expanded_references]
@@ -545,12 +547,32 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(page).to have_text('@abc123')
end
+ context 'when `disable_all_mention` is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+ end
+
+ it 'shows suggestions for members with descriptions' do
+ type_in_content_editor '@a'
+
+ expect(find(suggestions_dropdown)).to have_text('abc123')
+ expect(find(suggestions_dropdown)).not_to have_text('All Group Members')
+
+ type_in_content_editor 'bc'
+
+ send_keys [:arrow_down, :enter]
+
+ expect(page).not_to have_css(suggestions_dropdown)
+ expect(page).to have_text('@abc123')
+ end
+ end
+
it 'shows suggestions for merge requests' do
type_in_content_editor '!'
expect(find(suggestions_dropdown)).to have_text('My Cool Merge Request')
- send_keys :enter
+ send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('!1')
@@ -561,7 +583,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(find(suggestions_dropdown)).to have_text('My Cool Linked Issue')
- send_keys :enter
+ send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('#1')
@@ -572,7 +594,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(find(suggestions_dropdown)).to have_text('My Cool Milestone')
- send_keys :enter
+ send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('%My Cool Milestone')
@@ -584,7 +606,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(find(suggestions_dropdown)).to have_text('🙂 slight_smile')
expect(find(suggestions_dropdown)).to have_text('😸 smile_cat')
- send_keys :enter
+ send_keys [:arrow_down, :enter]
expect(page).not_to have_css(suggestions_dropdown)
@@ -614,7 +636,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
end
def dropdown_scroll_top
- evaluate_script("document.querySelector('#{suggestions_dropdown} .gl-dropdown-inner').scrollTop")
+ evaluate_script("document.querySelector('#{suggestions_dropdown}').scrollTop")
end
end
end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index d6f1efc09fc..430a8ac39d7 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -3,8 +3,8 @@
RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name|
let(:form_selector) { '.js-main-target-form' }
let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
- let(:toggle_selector) { "#{dropdown_selector} .gl-dropdown-toggle" }
- let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" }
+ let(:toggle_selector) { "#{dropdown_selector} .gl-new-dropdown-toggle" }
+ let(:menu_selector) { "#{dropdown_selector} .gl-new-dropdown-contents" }
let(:submit_selector) { "#{form_selector} .js-comment-submit-button > button:first-child" }
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
let(:comments_selector) { '.timeline > .note.timeline-entry:not(.being-posted)' }
@@ -63,33 +63,6 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name
expect(page).not_to have_selector menu_selector
end
- it 'clicking the ul padding or divider should not change the text' do
- execute_script("document.querySelector('#{menu_selector}').click()")
-
- # on issues page, the menu closes when clicking anywhere, on other pages it will
- # remain open if clicking divider or menu padding, but should not change button action
- #
- # if dropdown menu is not toggled (and also not present),
- # it's "issue-type" dropdown
- if first(menu_selector, minimum: 0).nil?
- expect(find(dropdown_selector)).to have_content 'Comment'
-
- find(toggle_selector).click
- execute_script("document.querySelector('#{menu_selector} .dropdown-divider').click()")
- else
- execute_script("document.querySelector('#{menu_selector}').click()")
-
- expect(page).to have_selector menu_selector
- expect(find(dropdown_selector)).to have_content 'Comment'
-
- execute_script("document.querySelector('#{menu_selector} .dropdown-divider').click()")
-
- expect(page).to have_selector menu_selector
- end
-
- expect(find(dropdown_selector)).to have_content 'Comment'
- end
-
describe 'when selecting "Start thread"' do
before do
find("#{menu_selector} li", match: :first)
@@ -177,21 +150,27 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name
end
RSpec.shared_examples 'thread comments for issue, epic and merge request' do |resource_name|
+ include ContentEditorHelpers
+
let(:form_selector) { '.js-main-target-form' }
- let(:dropdown_selector) { "#{form_selector} [data-testid='comment-button']" }
- let(:submit_button_selector) { "#{dropdown_selector} .split-content-button" }
- let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle-split" }
- let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" }
+ let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
+ let(:toggle_selector) { "#{dropdown_selector} .gl-new-dropdown-toggle" }
+ let(:menu_selector) { "#{dropdown_selector} .gl-new-dropdown-contents" }
+ let(:submit_selector) { "#{form_selector} .js-comment-submit-button > button:first-child" }
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
let(:comments_selector) { '.timeline > .note.timeline-entry:not(.being-posted)' }
let(:comment) { 'My comment' }
+ before do
+ close_rich_text_promo_popover_if_present
+ end
+
it 'clicking "Comment" will post a comment' do
expect(page).to have_selector toggle_selector
find("#{form_selector} .note-textarea").send_keys(comment)
- find(submit_button_selector).click
+ find(submit_selector).click
wait_for_all_requests
@@ -260,7 +239,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
describe 'creating a thread' do
before do
- find(submit_button_selector).click
+ find(submit_selector).click
wait_for_requests
find(comments_selector, match: :first)
@@ -284,7 +263,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
expect(new_comment).to have_css('.discussion-with-resolve-btn')
end
- if resource_name =~ /(issue|merge request)/
+ if /(issue|merge request)/.match?(resource_name)
it 'can be replied to' do
submit_reply('some text')
@@ -366,14 +345,14 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
end
it 'updates the submit button text and closes the dropdown' do
- button = find(submit_button_selector)
+ button = find(submit_selector)
expect(button).to have_content 'Comment'
expect(page).not_to have_selector menu_selector
end
- if resource_name =~ /(issue|merge request)/
+ if /(issue|merge request)/.match?(resource_name)
it 'updates the close button text' do
expect(find(close_selector)).to have_content "Comment & close #{resource_name}"
end
@@ -402,7 +381,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
end
end
- if resource_name =~ /(issue|merge request)/
+ if /(issue|merge request)/.match?(resource_name)
describe "on a closed #{resource_name}" do
before do
find("#{form_selector} .js-note-target-close").click
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index 14e53dc8655..f802404518b 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -125,7 +125,11 @@ RSpec.shared_examples 'an editable merge request' do
it 'allows to unselect "Remove source branch"', :js do
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
- visit edit_project_merge_request_path(target_project, merge_request)
+ begin
+ visit edit_project_merge_request_path(target_project, merge_request)
+ rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError
+ end
+
uncheck 'Delete source branch when merge request is accepted'
click_button 'Save changes'
diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
index 2eca2a72997..178f85cb85b 100644
--- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
@@ -159,6 +159,40 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
end
end
+ context 'when a user already exists, and private email is used' do
+ it 'fails with an error', :js do
+ visit subentity_members_page_path
+
+ invite_member(user2.email, role: role)
+
+ invite_modal = page.find(invite_modal_selector)
+ expect(invite_modal).to have_content "#{user2.email}: Access level should be greater than or equal to " \
+ "Developer inherited membership from group #{group.name}"
+
+ page.refresh
+
+ page.within find_invited_member_row(user2.name) do
+ expect(page).to have_content('Developer')
+ expect(page).not_to have_button('Developer')
+ end
+ end
+
+ it 'does not allow inviting of an email that has spaces', :js do
+ visit subentity_members_page_path
+
+ click_on _('Invite members')
+
+ page.within invite_modal_selector do
+ choose_options(role, nil)
+ find(member_dropdown_selector).set("#{user2.email} ")
+ wait_for_requests
+
+ expect(page).to have_content('No matches found')
+ expect(page).not_to have_button("#{user2.email} ")
+ end
+ end
+ end
+
context 'when there are multiple users invited with errors' do
let_it_be(:user3) { create(:user) }
diff --git a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
index d21bf62ecfa..53498a1bb39 100644
--- a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
+++ b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
RSpec.shared_examples 'milestone handling version conflicts' do
- it 'warns about version conflict when milestone has been updated in the background' do
+ it 'warns about version conflict when milestone has been updated in the background', :js do
+ wait_for_all_requests
+
# Update the milestone in the background in order to trigger a version conflict
milestone.update!(title: "New title")
diff --git a/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
new file mode 100644
index 00000000000..34821fb9eda
--- /dev/null
+++ b/spec/support/shared_examples/features/nav_sidebar_shared_examples.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'page has active tab' do |title|
+ it "activates #{title} tab" do
+ expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(find('.sidebar-top-level-items > li.active')).to have_content(title)
+ end
+end
+
+RSpec.shared_examples 'page has active sub tab' do |title|
+ it "activates #{title} sub tab" do
+ expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
+ expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
+ .to have_content(title)
+ end
+end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 5126e849c2e..8e8e7e8ad05 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
expect(package_row).to have_content(pkg.name)
expect(package_row).to have_content(pkg.version)
- expect(package_row).to have_content(pkg.project.path) if check_project_name
+ expect(package_row).to have_content(pkg.project.name) if check_project_name
end
end
diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
index 7737f8a73c5..806ffdad2f1 100644
--- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
@@ -4,8 +4,8 @@ RSpec.shared_examples 'it uploads and commits a new text file' do |drop: false|
it 'uploads and commits a new text file', :js do
find('.add-to-tree').click
- page.within('.dropdown-menu') do
- click_link('Upload file')
+ page.within('.repo-breadcrumb') do
+ click_button('Upload file')
wait_for_requests
end
@@ -40,8 +40,8 @@ RSpec.shared_examples 'it uploads and commits a new image file' do |drop: false|
it 'uploads and commits a new image file', :js do
find('.add-to-tree').click
- page.within('.dropdown-menu') do
- click_link('Upload file')
+ page.within('.repo-breadcrumb') do
+ click_button('Upload file')
wait_for_requests
end
@@ -70,8 +70,8 @@ RSpec.shared_examples 'it uploads and commits a new pdf file' do |drop: false|
it 'uploads and commits a new pdf file', :js do
find('.add-to-tree').click
- page.within('.dropdown-menu') do
- click_link('Upload file')
+ page.within('.repo-breadcrumb') do
+ click_button('Upload file')
wait_for_requests
end
@@ -111,7 +111,7 @@ RSpec.shared_examples 'it uploads and commits a new file to a forked project' do
wait_for_all_requests
find('.add-to-tree').click
- click_link('Upload file')
+ click_button('Upload file')
if drop
find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
@@ -149,7 +149,7 @@ RSpec.shared_examples 'it uploads a file to a sub-directory' do |drop: false|
end
find('.add-to-tree').click
- click_link('Upload file')
+ click_button('Upload file')
if drop
find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
diff --git a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb
index 337b3f3cbd0..7e3b507c1ba 100644
--- a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb
+++ b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb
@@ -24,6 +24,6 @@ RSpec.shared_examples 'creating an issue for a thread' do
expect(discussion.resolved?).to eq(true)
# Issue title inludes MR title
- expect(page).to have_content(%Q(Follow-up from "#{merge_request.title}"))
+ expect(page).to have_content(%(Follow-up from "#{merge_request.title}"))
end
end
diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb
index f6566214e32..a6b9c98923a 100644
--- a/spec/support/shared_examples/features/rss_shared_examples.rb
+++ b/spec/support/shared_examples/features/rss_shared_examples.rb
@@ -2,20 +2,20 @@
RSpec.shared_examples "an autodiscoverable RSS feed with current_user's feed token" do
it "has an RSS autodiscovery link tag with current_user's feed token" do
- expect(page).to have_css("link[type*='atom+xml'][href*='feed_token=#{user.feed_token}']", visible: false)
+ expect(page).to have_css("link[type*='atom+xml'][href*='feed_token=glft-'][href*='-#{user.id}']", visible: false)
end
end
RSpec.shared_examples "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
expect(page)
- .to have_css("a:has([data-testid='rss-icon'])[href*='feed_token=#{user.feed_token}']")
+ .to have_css("a:has([data-testid='rss-icon'])[href*='feed_token=glft-'][href*='-#{user.id}']")
end
end
RSpec.shared_examples "it has an RSS link with current_user's feed token" do
it "shows the RSS link with current_user's feed token" do
- expect(page).to have_link 'Subscribe to RSS feed', href: /feed_token=#{user.feed_token}/
+ expect(page).to have_link 'Subscribe to RSS feed', href: /feed_token=glft-.*-#{user.id}/
end
end
@@ -51,11 +51,17 @@ RSpec.shared_examples "updates atom feed link" do |type|
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expected = {
- 'feed_token' => [user.feed_token],
'assignee_id' => [user.id.to_s]
}
expect(params).to include(expected)
+ feed_token_param = params['feed_token']
+ expect(feed_token_param).to match([Gitlab::Auth::AuthFinders::PATH_DEPENDENT_FEED_TOKEN_REGEX])
+ expect(feed_token_param.first).to end_with(user.id.to_s)
+
expect(auto_discovery_params).to include(expected)
+ feed_token_param = auto_discovery_params['feed_token']
+ expect(feed_token_param).to match([Gitlab::Auth::AuthFinders::PATH_DEPENDENT_FEED_TOKEN_REGEX])
+ expect(feed_token_param.first).to end_with(user.id.to_s)
end
end
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb
index 206116d66c8..865f5aff476 100644
--- a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'date sidebar widget' do
wait_for_requests
- expect(page).to have_content(today.to_s(:medium))
+ expect(page).to have_content(today.to_fs(:medium))
expect(due_date_value.text).to have_content Time.current.strftime('%b %-d, %Y')
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
index ed885d7a226..c3df89c8002 100644
--- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
@@ -6,6 +6,7 @@
RSpec.shared_examples 'User creates wiki page' do
include WikiHelpers
+ include ContentEditorHelpers
before do
sign_in(user)
@@ -18,6 +19,7 @@ RSpec.shared_examples 'User creates wiki page' do
wait_for_svg_to_be_loaded(example)
click_link "Create your first page"
+ close_rich_text_promo_popover_if_present
end
it 'shows all available formats in the dropdown' do
@@ -190,6 +192,7 @@ RSpec.shared_examples 'User creates wiki page' do
context "via the `new wiki page` page", :js do
it "creates a page with a single word" do
click_link("New page")
+ close_rich_text_promo_popover_if_present
page.within(".wiki-form") do
fill_in(:wiki_title, with: "foo")
@@ -208,6 +211,7 @@ RSpec.shared_examples 'User creates wiki page' do
it "creates a page with spaces in the name", :js do
click_link("New page")
+ close_rich_text_promo_popover_if_present
page.within(".wiki-form") do
fill_in(:wiki_title, with: "Spaces in the name")
@@ -226,6 +230,7 @@ RSpec.shared_examples 'User creates wiki page' do
it "creates a page with hyphens in the name", :js do
click_link("New page")
+ close_rich_text_promo_popover_if_present
page.within(".wiki-form") do
fill_in(:wiki_title, with: "hyphens-in-the-name")
@@ -249,6 +254,7 @@ RSpec.shared_examples 'User creates wiki page' do
context 'when a server side validation error is returned' do
it "still displays edit form", :js do
click_link("New page")
+ close_rich_text_promo_popover_if_present
page.within(".wiki-form") do
fill_in(:wiki_title, with: "home")
@@ -266,6 +272,7 @@ RSpec.shared_examples 'User creates wiki page' do
it "shows the emoji autocompletion dropdown", :js do
click_link("New page")
+ close_rich_text_promo_popover_if_present
page.within(".wiki-form") do
find("#wiki_content").native.send_keys("")
diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
index ca68df9a89b..827c875494a 100644
--- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
@@ -5,6 +5,8 @@
# user
RSpec.shared_examples 'User previews wiki changes' do
+ include ContentEditorHelpers
+
let(:wiki_page) { build(:wiki_page, wiki: wiki) }
before do
@@ -74,6 +76,7 @@ RSpec.shared_examples 'User previews wiki changes' do
before do
wiki_page.create # rubocop:disable Rails/SaveBang
visit wiki_page_path(wiki, wiki_page, action: :edit)
+ close_rich_text_promo_popover_if_present
end
it_behaves_like 'relative links' do
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 91cacaf9209..d06f04db1ce 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
@@ -6,6 +6,8 @@
RSpec.shared_examples 'User updates wiki page' do
include WikiHelpers
+ include ContentEditorHelpers
+
let(:diagramsnet_url) { 'https://embed.diagrams.net' }
before do
@@ -21,6 +23,7 @@ RSpec.shared_examples 'User updates wiki page' do
wait_for_svg_to_be_loaded(example)
click_link "Create your first page"
+ close_rich_text_promo_popover_if_present
end
it 'redirects back to the home edit page' do
@@ -67,6 +70,7 @@ RSpec.shared_examples 'User updates wiki page' do
visit(wiki_path(wiki))
click_link('Edit')
+ close_rich_text_promo_popover_if_present
end
it 'updates a page', :js do
@@ -126,10 +130,6 @@ RSpec.shared_examples 'User updates wiki page' do
expect(page).to have_content('Updated Wiki Content')
end
- it 'focuses on the content field', :js do
- expect(page).to have_selector '.note-textarea:focus'
- end
-
it 'cancels editing of a page' do
page.within(:css, '.wiki-form .form-actions') do
click_on('Cancel')
@@ -164,6 +164,7 @@ RSpec.shared_examples 'User updates wiki page' do
before do
visit wiki_page_path(wiki, wiki_page, action: :edit)
+ close_rich_text_promo_popover_if_present
end
it 'moves the page to the root folder', :js do
@@ -234,6 +235,7 @@ RSpec.shared_examples 'User updates wiki page' do
stub_application_setting(wiki_page_max_content_bytes: 10)
visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit)
+ close_rich_text_promo_popover_if_present
end
it 'allows changing the title if the content does not change', :js do
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
index 767caffd417..3ee7725305e 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -6,6 +6,7 @@
RSpec.shared_examples 'User views a wiki page' do
include WikiHelpers
+ include ContentEditorHelpers
let(:path) { 'image.png' }
let(:wiki_page) do
@@ -269,6 +270,7 @@ RSpec.shared_examples 'User views a wiki page' do
wait_for_svg_to_be_loaded
click_link "Create your first page"
+ close_rich_text_promo_popover_if_present
expect(page).to have_content('Create New Page')
end
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 128bd28410c..4c15b682458 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -166,7 +166,8 @@ RSpec.shared_examples 'work items comments' do |type|
end
RSpec.shared_examples 'work items assignees' do
- it 'successfully assigns the current user by searching' do
+ it 'successfully assigns the current user by searching',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do
# The button is only when the mouse is over the input
find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username)
wait_for_requests
diff --git a/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb b/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
index 5cbbed1468f..38954e6f9cc 100644
--- a/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
+++ b/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
@@ -42,6 +42,12 @@ RSpec.shared_examples 'no assignee filter' do
expect(issuables).to contain_exactly(*expected_issuables)
end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_wildcard_id] = 'none'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
end
RSpec.shared_examples 'any assignee filter' do
@@ -57,5 +63,11 @@ RSpec.shared_examples 'any assignee filter' do
expect(issuables).to contain_exactly(*expected_issuables)
end
+
+ it 'returns issuables assigned to any assignee' do
+ params[:assignee_wildcard_id] = 'any'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
end
end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 67fed00b5ca..30041456d00 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -663,18 +663,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
expect(items).to contain_exactly(japanese)
end
end
-
- context 'when full-text search is disabled' do
- let(:search_term) { 'ometh' }
-
- before do
- stub_feature_flags(issues_full_text_search: false)
- end
-
- it 'allows partial word matches' do
- expect(items).to contain_exactly(english)
- end
- end
end
context 'filtering by item term in title' do
diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb
index 030a2feafcd..809c801de62 100644
--- a/spec/support/shared_examples/graphql/label_fields.rb
+++ b/spec/support/shared_examples/graphql/label_fields.rb
@@ -93,7 +93,7 @@ RSpec.shared_examples 'querying a GraphQL type with labels' do
describe 'performance' do
def query_for(*labels)
selections = labels.map do |label|
- %Q[#{label.title.gsub(/:+/, '_')}: label(title: "#{label.title}") { description }]
+ %[#{label.title.gsub(/:+/, '_')}: label(title: "#{label.title}") { description }]
end
make_query(selections)
diff --git a/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
index b096a5e17c0..13d2447754c 100644
--- a/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
@@ -33,6 +33,10 @@ RSpec.shared_examples 'board lists create mutation' do
describe 'backlog list' do
let(:list_create_params) { { backlog: true } }
+ before do
+ board.lists.backlog.delete_all
+ end
+
it 'creates one and only one backlog' do
expect { subject }.to change { board.lists.backlog.count }.by(1)
expect(board.lists.backlog.first.list_type).to eq 'backlog'
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
index 30212e44c6a..c666b72d492 100644
--- 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
@@ -157,10 +157,10 @@ RSpec.shared_examples 'work item supports type change via quick actions' do
let_it_be(:assignee) { create(:user) }
let_it_be(:task_type) { WorkItems::Type.default_by_type(:task) }
- let(:body) { "Updating type.\n/type Issue" }
+ let(:body) { "Updating type.\n/type issue" }
before do
- noteable.update!(work_item_type: task_type, issue_type: task_type.base_type)
+ noteable.update!(work_item_type: task_type)
end
it 'updates type' do
@@ -211,4 +211,15 @@ RSpec.shared_examples 'work item supports type change via quick actions' do
.to include("Commands only Type changed successfully. Assigned @#{assignee.username}.")
end
end
+
+ context 'when the type name is upper case' do
+ let(:body) { "Updating type.\n/type Issue" }
+
+ it 'changes type to issue' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
+ end
+ 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 d8cc6f697d7..c32e758d921 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
@@ -50,6 +50,8 @@ RSpec.shared_examples "a user type with merge request interaction type" do
organization
jobTitle
createdAt
+ pronouns
+ ide
]
# TODO: 'workspaces' needs to be included, but only when this spec is run in EE context, to account for the
diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
index 47655f86558..e6433f963f4 100644
--- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'default allowlist' do
expect(filter(act).to_html).to eq exp
end
- it 'allows whitelisted HTML tags from the user' do
+ it 'allows allowlisted HTML tags from the user' do
exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>"
expect(filter(act).to_html).to eq exp
end
@@ -110,7 +110,7 @@ RSpec.shared_examples 'XSS prevention' do
},
'protocol-based JS injection: Unicode' => {
- input: %Q(<a href="\u0001java\u0003script:alert('XSS')">foo</a>),
+ input: %(<a href="\u0001java\u0003script:alert('XSS')">foo</a>),
output: '<a>foo</a>'
},
diff --git a/spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb
deleted file mode 100644
index a1e75e4af7e..00000000000
--- a/spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'foreign key 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_query) do
- [
- {
- 'schema' => schema,
- 'table_name' => 'web_hooks',
- 'foreign_key_name' => 'web_hooks_project_id_fkey',
- 'foreign_key_definition' => 'FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE'
- },
- {
- 'schema' => schema,
- 'table_name' => 'issues',
- 'foreign_key_name' => 'wrong_definition_fk',
- 'foreign_key_definition' => 'FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE'
- },
- {
- 'schema' => schema,
- 'table_name' => 'projects',
- 'foreign_key_name' => 'extra_fk',
- 'foreign_key_definition' => 'FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE'
- }
- ]
- end
-
- before do
- allow(connection).to receive(:exec_query).and_return(database_query)
- 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/database/index_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb
deleted file mode 100644
index 6f0cede7130..00000000000
--- a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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/table_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb
deleted file mode 100644
index 96e58294675..00000000000
--- a/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples "table 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(:inconsistency_type) { validator.name.demodulize.underscore }
- let(:database_model) { Gitlab::Database.database_base_models['main'] }
- 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) }
- let(:database_tables) do
- [
- {
- 'table_name' => 'wrong_table',
- 'column_name' => 'id',
- 'not_null' => true,
- 'data_type' => 'integer',
- 'column_default' => "nextval('audit_events_id_seq'::regclass)"
- },
- {
- 'table_name' => 'wrong_table',
- 'column_name' => 'description',
- 'not_null' => true,
- 'data_type' => 'character varying',
- 'column_default' => nil
- },
- {
- 'table_name' => 'extra_table',
- 'column_name' => 'id',
- 'not_null' => true,
- 'data_type' => 'integer',
- 'column_default' => "nextval('audit_events_id_seq'::regclass)"
- },
- {
- 'table_name' => 'extra_table',
- 'column_name' => 'email',
- 'not_null' => true,
- 'data_type' => 'character varying',
- 'column_default' => nil
- },
- {
- 'table_name' => 'extra_table_columns',
- 'column_name' => 'id',
- 'not_null' => true,
- 'data_type' => 'bigint',
- 'column_default' => "nextval('audit_events_id_seq'::regclass)"
- },
- {
- 'table_name' => 'extra_table_columns',
- 'column_name' => 'name',
- 'not_null' => true,
- 'data_type' => 'character varying(255)',
- 'column_default' => nil
- },
- {
- 'table_name' => 'extra_table_columns',
- 'column_name' => 'extra_column',
- 'not_null' => true,
- 'data_type' => 'character varying(255)',
- 'column_default' => nil
- },
- {
- 'table_name' => 'missing_table_columns',
- 'column_name' => 'id',
- 'not_null' => true,
- 'data_type' => 'bigint',
- 'column_default' => 'NOT NULL'
- }
- ]
- end
-
- before do
- allow(connection).to receive(:exec_query).and_return(database_tables)
- end
-
- it 'returns table 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/trigger_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb
deleted file mode 100644
index 13a112275c2..00000000000
--- a/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
index 169fceced7a..9dc18555340 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
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'tracked issuable snowplow and service ping events for given event params' do
+RSpec.shared_examples 'tracked issuable events' do
before do
stub_application_setting(usage_ping_enabled: true)
end
@@ -21,6 +21,10 @@ RSpec.shared_examples 'tracked issuable snowplow and service ping events for giv
it 'does not track edit actions if author is not present' do
expect(track_action({ author: nil }.merge(track_params))).to be_nil
end
+end
+
+RSpec.shared_examples 'tracked issuable snowplow and service ping events for given event params' do
+ it_behaves_like 'tracked issuable events'
it 'emits snowplow event' do
track_action({ author: user1 }.merge(track_params))
@@ -29,6 +33,23 @@ RSpec.shared_examples 'tracked issuable snowplow and service ping events for giv
end
end
+RSpec.shared_examples 'tracked issuable internal event for given event params' do
+ it_behaves_like 'tracked issuable events'
+
+ it_behaves_like 'internal event tracking' do
+ subject(:track_event) { track_action({ author: user1 }.merge(track_params)) }
+
+ let(:user) { user1 }
+ let(:namespace) { project&.namespace }
+ end
+end
+
+RSpec.shared_examples 'tracked issuable internal event with project' do
+ it_behaves_like 'tracked issuable internal event for given event params' do
+ let(:track_params) { original_params || { project: project } }
+ end
+end
+
RSpec.shared_examples 'tracked issuable snowplow and service ping events with project' do
it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do
let(:context) do
diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
index 6f104f400bc..52f0e7847b0 100644
--- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
+++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'StageEventModel' do
describe '.upsert_data' do
- let(:time) { Time.parse(Time.current.to_s(:db)) } # truncating the timestamp so we can compare it with the timestamp loaded from the DB
+ let(:time) { Time.parse(Time.current.to_fs(:db)) } # truncating the timestamp so we can compare it with the timestamp loaded from the DB
let(:input_data) do
[
{
diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb
index 4753d7a4556..0e9200f1fd9 100644
--- a/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb
+++ b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb
@@ -6,16 +6,34 @@ RSpec.shared_examples 'protected ref access' do |association|
let_it_be(:project) { create(:project) }
let_it_be(:protected_ref) { create(association, project: project) } # rubocop:disable Rails/SaveBang
- it { is_expected.to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) }
+ describe 'validations' do
+ subject { build(described_class.model_name.singular) }
- it { is_expected.to validate_presence_of(:access_level) }
+ context 'when role?' do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) }
- context 'when not role?' do
- before do
- allow(subject).to receive(:role?).and_return(false)
+ it { is_expected.to validate_presence_of(:access_level) }
+
+ it do
+ is_expected.to validate_uniqueness_of(:access_level)
+ .scoped_to("#{described_class.module_parent.model_name.singular}_id")
+ end
end
- it { is_expected.not_to validate_presence_of(:access_level) }
+ context 'when not role?' do
+ before do
+ allow(subject).to receive(:role?).and_return(false)
+ end
+
+ it { is_expected.not_to validate_presence_of(:access_level) }
+
+ it { is_expected.not_to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) }
+
+ it do
+ is_expected.not_to validate_uniqueness_of(:access_level)
+ .scoped_to("#{described_class.module_parent.model_name.singular}_id")
+ end
+ end
end
describe '::human_access_levels' do
diff --git a/spec/support/shared_examples/models/concerns/protected_ref_deploy_key_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_deploy_key_access_examples.rb
new file mode 100644
index 00000000000..f2e79dc377b
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/protected_ref_deploy_key_access_examples.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'protected ref deploy_key access' do
+ let_it_be(:described_instance) { described_class.model_name.singular }
+ let_it_be(:protected_ref_name) { described_class.module_parent.model_name.singular }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:protected_ref) { create(protected_ref_name, project: project) } # rubocop:disable Rails/SaveBang
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:deploy_key) }
+ end
+
+ describe 'validations' do
+ context 'when deploy_key?' do
+ context 'when deploy key enabled for the project' do
+ let(:deploy_key) do
+ create(:deploy_keys_project, :write_access, project: project).deploy_key
+ end
+
+ it 'is valid' do
+ level = build(described_instance, protected_ref_name => protected_ref, deploy_key: deploy_key)
+
+ expect(level).to be_valid
+ end
+ end
+
+ context 'when a deploy key already added for this access level' do
+ let(:deploy_key) { create(:deploy_keys_project, :write_access, project: project).deploy_key }
+
+ before do
+ create(described_instance, protected_ref_name => protected_ref, deploy_key: deploy_key)
+ end
+
+ subject(:access_level) do
+ build(described_instance, protected_ref_name => protected_ref, deploy_key: deploy_key)
+ end
+
+ it 'is not valid', :aggregate_failures do
+ is_expected.to be_invalid
+ expect(access_level.errors.full_messages).to contain_exactly('Deploy key has already been taken')
+ end
+ end
+
+ context 'when deploy key is not enabled for the project' do
+ subject(:access_level) do
+ build(described_instance, protected_ref_name => protected_ref, deploy_key: create(:deploy_key))
+ end
+
+ it 'is not valid', :aggregate_failures do
+ is_expected.to be_invalid
+ expect(access_level.errors.full_messages).to contain_exactly('Deploy key is not enabled for this project')
+ end
+ end
+
+ context 'when deploy key is not active for the project' do
+ subject(:access_level) do
+ deploy_key = create(:deploy_keys_project, :readonly_access, project: project).deploy_key
+ build(described_instance, protected_ref_name => protected_ref, deploy_key: deploy_key)
+ end
+
+ it 'is not valid', :aggregate_failures do
+ is_expected.to be_invalid
+ expect(access_level.errors.full_messages).to contain_exactly('Deploy key is not enabled for this project')
+ end
+ end
+ end
+ end
+
+ describe '#check_access' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:deploy_key) { create(:deploy_key, user: user) }
+ let_it_be(:deploy_keys_project) do
+ create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key)
+ end
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ context "when this #{described_class.model_name.singular} is tied to a deploy key" do
+ let!(:access_level) do
+ create(described_instance, protected_ref_name => protected_ref, deploy_key: deploy_key)
+ end
+
+ context 'when the deploy key is among the active keys for this project' do
+ it { expect(access_level.check_access(user)).to be_truthy }
+ end
+
+ context 'when user is missing' do
+ it { expect(access_level.check_access(nil)).to be_falsey }
+ end
+
+ context 'when deploy key does not belong to the user' do
+ let(:another_user) { create(:user) }
+
+ it { expect(access_level.check_access(another_user)).to be_falsey }
+ end
+
+ context 'when user cannot access the project' do
+ before do
+ allow(user).to receive(:can?).with(:read_project, project).and_return(false)
+ end
+
+ it { expect(access_level.check_access(user)).to be_falsey }
+ end
+
+ context 'when the deploy key is not among the active keys of this project' do
+ before do
+ deploy_keys_project.update!(can_push: false)
+ end
+
+ after do
+ deploy_keys_project.update!(can_push: true)
+ end
+
+ it { expect(access_level.check_access(user)).to be_falsey }
+ end
+ end
+ end
+
+ describe '#type' do
+ let(:access_level) { build(described_instance) }
+
+ context 'when deploy_key?' do
+ let(:access_level) { build(described_instance, deploy_key: build(:deploy_key)) }
+
+ it 'returns :deploy_key' do
+ expect(access_level.type).to eq(:deploy_key)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb b/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb
index 3d98d9136e2..56b36b3ea07 100644
--- a/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb
@@ -12,7 +12,6 @@ RSpec.shared_examples 'database events tracking' do
let(:category) { described_class.to_s }
let(:label) { described_class.table_name }
let(:action) { "database_event_#{property}" }
- let(:feature_flag_name) { :product_intelligence_database_event_tracking }
let(:record_tracked_attributes) { record.attributes.slice(*described_class::SNOWPLOW_ATTRIBUTES.map(&:to_s)) }
let(:base_extra) { record_tracked_attributes.merge(project: try(:project), namespace: try(:namespace)) }
@@ -48,9 +47,3 @@ RSpec.shared_examples 'database events tracking' do
end
end
end
-
-RSpec.shared_examples 'database events tracking batch 2' do
- it_behaves_like 'database events tracking' do
- let(:feature_flag_name) { :product_intelligence_database_event_tracking_batch2 }
- 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 0cf109ce5c5..ff3cc1841b4 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
@@ -122,7 +122,7 @@ RSpec.shared_examples Integrations::BaseSlashCommands do
end
it_behaves_like 'blocks command execution' do
- let(:error_message) { 'your account has been deactivated by your administrator' }
+ let(:error_message) { "your #{Gitlab.config.gitlab.url} account needs to be reactivated" }
end
end
end
diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb
index 600539f7d0a..4dff4f68995 100644
--- a/spec/support/shared_examples/namespaces/traversal_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_examples.rb
@@ -239,13 +239,11 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#ancestors_upto' do
- context 'with use_traversal_ids_for_ancestors_upto enabled' do
- include_examples '#ancestors_upto'
- end
+ include_examples '#ancestors_upto'
- context 'with use_traversal_ids_for_ancestors_upto disabled' do
+ context 'with use_traversal_ids disabled' do
before do
- stub_feature_flags(use_traversal_ids_for_ancestors_upto: false)
+ stub_feature_flags(use_traversal_ids: false)
end
include_examples '#ancestors_upto'
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 0c4e5ce51fc..b308295b5fb 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -70,10 +70,9 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
describe '.roots' do
- context "use_traversal_ids_roots feature flag is true" do
+ context "use_traversal_ids feature flag is true" do
before do
stub_feature_flags(use_traversal_ids: true)
- stub_feature_flags(use_traversal_ids_roots: true)
end
it_behaves_like '.roots'
@@ -83,9 +82,9 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
end
- context "use_traversal_ids_roots feature flag is false" do
+ context "use_traversal_ids feature flag is false" do
before do
- stub_feature_flags(use_traversal_ids_roots: false)
+ stub_feature_flags(use_traversal_ids: false)
end
it_behaves_like '.roots'
diff --git a/spec/support/shared_examples/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/nav_sidebar_shared_examples.rb
deleted file mode 100644
index 4b815988bc5..00000000000
--- a/spec/support/shared_examples/nav_sidebar_shared_examples.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'has nav sidebar' do
- it 'has collapsed nav sidebar on mobile' do
- render
-
- expect(rendered).to have_selector('.nav-sidebar')
- expect(rendered).not_to have_selector('.sidebar-collapsed-desktop')
- expect(rendered).not_to have_selector('.sidebar-expanded-mobile')
- end
-end
-
-RSpec.shared_examples 'page has active tab' do |title|
- it "activates #{title} tab" do
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(title)
- end
-end
-
-RSpec.shared_examples 'page has active sub tab' do |title|
- it "activates #{title} sub tab" do
- expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
- .to have_content(title)
- end
-end
-
-RSpec.shared_examples 'sidebar includes snowplow attributes' do |track_action, track_label, track_property|
- specify do
- stub_application_setting(snowplow_enabled: true)
-
- render
-
- expect(rendered).to have_css(".nav-sidebar[data-track-action=\"#{track_action}\"][data-track-label=\"#{track_label}\"][data-track-property=\"#{track_property}\"]")
- end
-end
diff --git a/spec/support/shared_examples/npm_sync_metadata_cache_worker_shared_examples.rb b/spec/support/shared_examples/npm_sync_metadata_cache_worker_shared_examples.rb
new file mode 100644
index 00000000000..de2dc4c3725
--- /dev/null
+++ b/spec/support/shared_examples/npm_sync_metadata_cache_worker_shared_examples.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'does not enqueue a worker to sync a metadata cache' do
+ it 'does not enqueue a worker to sync a metadata cache' do
+ expect(Packages::Npm::CreateMetadataCacheWorker).not_to receive(:perform_async)
+
+ subject
+ end
+end
+
+RSpec.shared_examples 'enqueue a worker to sync a metadata cache' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'enqueues a worker to create a metadata cache' do
+ expect(Packages::Npm::CreateMetadataCacheWorker)
+ .to receive(:perform_async).with(project.id, package_name)
+
+ subject
+ end
+
+ context 'with npm_metadata_cache disabled' do
+ before do
+ stub_feature_flags(npm_metadata_cache: false)
+ end
+
+ it_behaves_like 'does not enqueue a worker to sync a metadata cache'
+ 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 9d6e7e75f4d..9002ccd5878 100644
--- a/spec/support/shared_examples/observability/csp_shared_examples.rb
+++ b/spec/support/shared_examples/observability/csp_shared_examples.rb
@@ -5,23 +5,28 @@
# It requires the following variables declared in the context including this example:
#
# - `tested_path`: the path under test
-# - `user`: the test user
-# - `group`: the test group
#
# e.g.
#
# ```
-# let_it_be(:group) { create(:group) }
-# let_it_be(:user) { create(:user) }
# it_behaves_like "observability csp policy" do
# let(:tested_path) { ....the path under test }
# end
# ```
#
+# Note that the context's user is expected to be logged-in and the
+# related resources (group, project, etc) are supposed to be provided with proper
+# permissions already, e.g.
+#
+# before do
+# login_as(user)
+# group.add_developer(user)
+# end
+#
# It optionally supports specifying the controller class handling the tested path as a parameter, e.g.
#
# ```
-# it_behaves_like "observability csp policy", Groups::ObservabilityController
+# it_behaves_like "observability csp policy", Projects::TracingController
# ```
# (If not specified it will default to `described_class`)
#
@@ -41,9 +46,6 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
before do
setup_csp_for_controller(controller_class, csp, any_time: true)
- group.add_developer(user)
- login_as(user)
- stub_feature_flags(observability_group_tab: true)
end
subject do
@@ -59,93 +61,127 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
end
end
- context 'when observability is disabled' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src 'https://something.test'
+ describe 'frame-src' do
+ context 'when frame-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src 'https://something.test'
+ end
end
- end
- before do
- stub_feature_flags(observability_group_tab: false)
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
+ end
end
- it 'does not add observability urls to the csp header' do
- expect(subject).to include("frame-src https://something.test")
- expect(subject).not_to include("#{observability_url} #{signin_url} #{oauth_url}")
- end
- end
+ context 'when signin url is already present in the policy' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src signin_url
+ end
+ end
- context 'when frame-src exists in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src 'https://something.test'
+ it 'does not append signin again' do
+ expect(subject).to include(
+ "frame-src #{signin_url} #{observability_url} #{oauth_url};")
end
end
- it 'appends the proper url to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
- end
- end
+ context 'when oauth url is already present in the policy' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src oauth_url
+ end
+ end
- context 'when signin is already present in the policy' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src signin_url
+ it 'does not append oauth again' do
+ expect(subject).to include(
+ "frame-src #{oauth_url} #{observability_url} #{signin_url};")
end
end
- it 'does not append signin again' do
- expect(subject).to include(
- "frame-src #{signin_url} #{observability_url} #{oauth_url};")
- end
- end
+ context 'when default-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something.test'
+ end
+ end
+
+ it 'does not change default-src' do
+ expect(subject).to include(
+ "default-src https://something.test;")
+ end
- context 'when oauth is already present in the policy' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src oauth_url
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
end
end
- it 'does not append oauth again' do
- expect(subject).to include(
- "frame-src #{oauth_url} #{observability_url} #{signin_url};")
+ context 'when frame-src and default-src exist in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something_default.test'
+ p.frame_src 'https://something.test'
+ end
+ end
+
+ it 'appends to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
+ expect(subject).to include(
+ "default-src https://something_default.test")
+ end
end
end
- context 'when default-src exists in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.default_src 'https://something.test'
+ describe 'connect-src' do
+ context 'when connect-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.connect_src 'https://something.test'
+ end
end
- end
- it 'does not change default-src' do
- expect(subject).to include(
- "default-src https://something.test;")
+ it 'appends the proper url to connect-src CSP directives' do
+ expect(subject).to include(
+ "connect-src https://something.test localhost #{observability_url}")
+ end
end
- it 'appends the proper url to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
- end
- end
+ context 'when default-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something.test'
+ end
+ end
+
+ it 'does not change default-src' do
+ expect(subject).to include(
+ "default-src https://something.test;")
+ end
- context 'when frame-src and default-src exist in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.default_src 'https://something_default.test'
- p.frame_src 'https://something.test'
+ it 'appends the proper url to connect-src CSP directives' do
+ expect(subject).to include(
+ "connect-src https://something.test localhost #{observability_url}")
end
end
- it 'appends to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
- expect(subject).to include(
- "default-src https://something_default.test")
+ context 'when connect-src and default-src exist in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something_default.test'
+ p.connect_src 'https://something.test'
+ end
+ end
+
+ it 'appends to connect-src CSP directives' do
+ expect(subject).to include(
+ "connect-src https://something.test localhost #{observability_url}")
+ expect(subject).to include(
+ "default-src https://something_default.test")
+ end
end
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index 7cbaf40721a..4b27f1f2520 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -2,6 +2,7 @@
RSpec.shared_examples 'close quick action' do |issuable_type|
include Features::NotesHelpers
+ include ContentEditorHelpers
before do
project.add_maintainer(maintainer)
@@ -76,6 +77,7 @@ RSpec.shared_examples 'close quick action' do |issuable_type|
context "preview of note on #{issuable_type}", :js do
it 'explains close quick action' do
visit public_send("project_#{issuable_type}_path", project, issuable)
+ close_rich_text_promo_popover_if_present
preview_note("this is done, close\n/close") do
expect(page).not_to have_content '/close'
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
index acbc6429646..cf5a67f6096 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -41,7 +41,13 @@ RSpec.shared_examples 'merge quick action' do
it 'schedules to merge the MR' do
add_note("/merge")
- expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)."
+ if Gitlab.ee?
+ expect(page).to(
+ have_content("Scheduled to merge this merge request (Merge when checks pass).")
+ )
+ else
+ expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)."
+ end
expect(merge_request.reload).to be_auto_merge_enabled
expect(merge_request.reload).not_to be_merged
diff --git a/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
index 9ccb7c0ae42..0fc914d71d5 100644
--- a/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
@@ -20,7 +20,7 @@ RSpec.shared_examples 'quick actions that change work item type' do
end
context 'when new type is the same as current type' do
- let(:command) { '/type Issue' }
+ let(:command) { '/type issue' }
it_behaves_like 'quick command error', 'Types are the same'
end
@@ -57,7 +57,7 @@ RSpec.shared_examples 'quick actions that change work item type' do
it 'populates :issue_type: and :work_item_type' do
_, updates, message = service.execute(command, work_item)
- expect(message).to eq(_('Work Item promoted successfully.'))
+ expect(message).to eq(_('Work item promoted successfully.'))
expect(updates).to eq({ issue_type: 'incident', work_item_type: WorkItems::Type.default_by_type(:incident) })
end
@@ -73,7 +73,7 @@ RSpec.shared_examples 'quick actions that change work item type' do
it 'populates :issue_type: and :work_item_type' do
_, updates, message = service.execute(command, work_item)
- expect(message).to eq(_('Work Item promoted successfully.'))
+ expect(message).to eq(_('Work item promoted successfully.'))
expect(updates).to eq({ issue_type: 'issue', work_item_type: WorkItems::Type.default_by_type(:issue) })
end
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 5cb6c3d310f..513f9802b34 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
@@ -20,7 +20,6 @@ RSpec.shared_examples 'Debian packages upload request' do |status, body = nil|
if status == :created
it 'creates package files', :aggregate_failures do
expect(::Packages::Debian::CreatePackageFileService).to receive(:new).with(package: be_a(Packages::Package), current_user: be_an(User), params: be_an(Hash)).and_call_original
- expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
if extra_params[:distribution] || file_name.end_with?('.changes')
expect(::Packages::Debian::FindOrCreateIncomingService).not_to receive(:new)
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
index 930c47dac52..6eceb7c350d 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -17,6 +17,10 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
describe 'filters' do
+ let(:mutually_exclusive_error) do
+ 'only one of [assigneeUsernames, assigneeUsername, assigneeWildcardId] arguments is allowed at the same time.'
+ end
+
before_all do
issue_a.assignee_ids = current_user.id
issue_b.assignee_ids = another_user.id
@@ -31,9 +35,45 @@ RSpec.shared_examples 'graphql issue list request spec' do
it 'returns a mutually exclusive param error' do
post_query
- expect_graphql_errors_to_include(
- 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.'
- )
+ expect_graphql_errors_to_include(mutually_exclusive_error)
+ end
+ end
+
+ context 'when both assignee_username and assignee_wildcard_id filters are provided' do
+ let(:issue_filter_params) do
+ { assignee_username: current_user.username, assignee_wildcard_id: :ANY }
+ end
+
+ it 'returns a mutually exclusive param error' do
+ post_query
+
+ expect_graphql_errors_to_include(mutually_exclusive_error)
+ end
+ end
+
+ context 'when filtering by assignee_wildcard_id' do
+ context 'when filtering for all issues with assignees' do
+ let(:issue_filter_params) do
+ { assignee_wildcard_id: :ANY }
+ end
+
+ it 'returns all issues with assignees' do
+ post_query
+
+ expect(issue_ids).to match_array([issue_a, issue_b].map { |i| i.to_gid.to_s })
+ end
+ end
+
+ context 'when filtering for issues without assignees' do
+ let(:issue_filter_params) do
+ { assignee_wildcard_id: :NONE }
+ end
+
+ it 'returns all issues without assignees' do
+ post_query
+
+ expect(issue_ids).to match_array([issue_c, issue_d, issue_e].map { |i| i.to_gid.to_s })
+ end
end
end
@@ -492,7 +532,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
before do
- issue_a.update_columns(issue_type: WorkItems::Type.base_types[:incident], work_item_type_id: incident_type.id)
+ issue_a.update_columns(work_item_type_id: incident_type.id)
end
it 'returns the escalation status values' do
diff --git a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
new file mode 100644
index 00000000000..7c32c7bf2a9
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'workspaces query in licensed environment and with feature flag on' do
+ describe 'when licensed and remote_development_feature_flag feature flag is enabled' do
+ before do
+ stub_licensed_features(remote_development: true)
+
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ # noinspection RubyResolve
+ it { is_expected.to match_array(a_hash_including('name' => workspace.name)) }
+ # noinspection RubyResolve
+
+ context 'when user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+end
+
+RSpec.shared_examples 'workspaces query in unlicensed environment and with feature flag off' do
+ describe 'when remote_development feature is unlicensed' do
+ before do
+ stub_licensed_features(remote_development: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns an error' do
+ expect(subject).to be_nil
+ expect_graphql_errors_to_include(/'remote_development' licensed feature is not available/)
+ end
+ end
+
+ describe 'when remote_development_feature_flag feature flag is disabled' do
+ before do
+ stub_licensed_features(remote_development: true)
+ stub_feature_flags(remote_development_feature_flag: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns an error' do
+ expect(subject).to be_nil
+ expect_graphql_errors_to_include(/'remote_development_feature_flag' feature flag is disabled/)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb
index 81ff004779a..47cbd268a65 100644
--- a/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb
@@ -27,7 +27,7 @@ RSpec.shared_examples 'creates model experiments package files' do
end
it 'returns bad request if package creation fails' do
- allow_next_instance_of(::Packages::MlModel::CreatePackageFileService) do |instance|
+ expect_next_instance_of(::Packages::MlModel::CreatePackageFileService) do |instance|
expect(instance).to receive(:execute).and_return(nil)
end
@@ -106,3 +106,19 @@ RSpec.shared_examples 'process ml model package upload' do
end
end
end
+
+RSpec.shared_examples 'process ml model package download' do
+ context 'when package file exists' do
+ it { is_expected.to have_gitlab_http_status(:success) }
+ end
+
+ context 'when record does not exist' do
+ it 'response is not found' do
+ expect_next_instance_of(::Packages::MlModel::PackageFinder) do |instance|
+ expect(instance).to receive(:execute!).and_raise(ActiveRecord::RecordNotFound)
+ end
+
+ expect(api_response).to have_gitlab_http_status(:not_found)
+ end
+ end
+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 5284ed2de21..6b6bf375827 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
@@ -44,11 +44,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
shared_examples 'reject metadata request' do |status:|
- it 'rejects the metadata request' do
- subject
-
- expect(response).to have_gitlab_http_status(status)
- end
+ it_behaves_like 'returning response status', status
end
shared_examples 'redirect metadata request' do |status:|
@@ -280,13 +276,15 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
example_name = 'redirect metadata request'
status = :redirected
else
- example_name = 'reject metadata request'
status = :not_found
end
end
status = :not_found if scope == :group && params[:package_name_type] == :non_existing && !params[:request_forward]
+ # Check the error message for :not_found
+ example_name = 'returning response status with error' if status == :not_found
+
it_behaves_like example_name, status: status
end
end
@@ -361,11 +359,11 @@ RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
end
shared_examples 'reject audit request' do |status:|
- it 'rejects the audit request' do
- subject
+ it_behaves_like 'returning response status', status
+ end
- expect(response).to have_gitlab_http_status(status)
- end
+ shared_examples 'reject audit request with error' do |status:|
+ it_behaves_like 'returning response status with error', status: status, error: 'Project not found'
end
shared_examples 'redirect audit request' do |status:|
@@ -464,7 +462,7 @@ RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
example_name = 'redirect audit request'
status = :temporary_redirect
else
- example_name = 'reject audit request'
+ example_name = 'reject audit request with error'
status = :not_found
end
end
@@ -633,12 +631,12 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
example_name = "#{params[:expected_result]} package tags request"
status = params[:expected_status]
- if scope == :instance && params[:package_name_type] != :scoped_naming_convention
- example_name = 'reject package tags request'
+ if (scope == :instance && params[:package_name_type] != :scoped_naming_convention) || (scope == :group && params[:package_name_type] == :non_existing)
status = :not_found
end
- status = :not_found if scope == :group && params[:package_name_type] == :non_existing
+ # Check the error message for :not_found
+ example_name = 'returning response status with error' if status == :not_found
it_behaves_like example_name, status: status
end
@@ -858,6 +856,9 @@ RSpec.shared_examples 'handling different package names, visibilities and user r
status = :not_found if scope == :group && params[:package_name_type] == :non_existing && params[:auth].present?
+ # Check the error message for :not_found
+ example_name = 'returning response status with error' if status == :not_found
+
it_behaves_like example_name, status: status
end
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
index 7c20ea661b5..403344efe03 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -36,18 +36,18 @@ RSpec.shared_examples 'accept package tags request' do |status:|
end
context 'with invalid package name' do
- where(:package_name, :status) do
- '%20' | :bad_request
- nil | :not_found
+ where(:package_name, :status, :error) do
+ '%20' | :bad_request | '"Package Name" not given'
+ nil | :not_found | %r{\A(Packages|Project) not found\z}
end
with_them do
- it_behaves_like 'returning response status', params[:status]
+ it_behaves_like 'returning response status with error', status: params[:status], error: params[:error]
end
end
end
-RSpec.shared_examples 'accept create package tag request' do |user_type|
+RSpec.shared_examples 'accept create package tag request' do |status:|
using RSpec::Parameterized::TableSyntax
context 'with valid package name' do
@@ -92,45 +92,55 @@ RSpec.shared_examples 'accept create package tag request' do |user_type|
expect(response.body).to be_empty
end
end
+
+ context 'with ActiveRecord::RecordInvalid error' do
+ before do
+ allow_next_instance_of(Packages::Tag) do |tag|
+ allow(tag).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ it_behaves_like 'returning response status with error', status: :bad_request, error: 'Record invalid'
+ end
end
context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
+ where(:package_name, :status, :error) do
+ 'unknown' | :not_found | %r{\A(Package|Project) not found\z}
+ '' | :not_found | '404 Not Found'
+ '%20' | :bad_request | '"Package Name" not given'
end
with_them do
- it_behaves_like 'returning response status', params[:status]
+ it_behaves_like 'returning response status with error', status: params[:status], error: params[:error]
end
end
context 'with invalid tag name' do
- where(:tag_name, :status) do
- '' | :not_found
- '%20' | :bad_request
+ where(:tag_name, :status, :error) do
+ '' | :not_found | '404 Not Found'
+ '%20' | :bad_request | '"Tag" not given'
end
with_them do
- it_behaves_like 'returning response status', params[:status]
+ it_behaves_like 'returning response status with error', status: params[:status], error: params[:error]
end
end
context 'with invalid version' do
- where(:version, :status) do
- ' ' | :bad_request
- '' | :bad_request
- nil | :bad_request
+ where(:version, :status, :error) do
+ ' ' | :bad_request | '"Version" not given'
+ '' | :bad_request | '"Version" not given'
+ nil | :bad_request | '"Version" not given'
end
with_them do
- it_behaves_like 'returning response status', params[:status]
+ it_behaves_like 'returning response status with error', status: params[:status], error: params[:error]
end
end
end
-RSpec.shared_examples 'accept delete package tag request' do |user_type|
+RSpec.shared_examples 'accept delete package tag request' do |status:|
using RSpec::Parameterized::TableSyntax
context 'with valid package name' do
@@ -159,29 +169,39 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type|
it_behaves_like 'returning response status', :not_found
end
+
+ context 'with ActiveRecord::RecordInvalid error' do
+ before do
+ allow_next_instance_of(::Packages::RemoveTagService) do |service|
+ allow(service).to receive(:execute).and_raise(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ it_behaves_like 'returning response status with error', status: :bad_request, error: 'Record invalid'
+ end
end
context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
+ where(:package_name, :status, :error) do
+ 'unknown' | :not_found | %r{\A(Package tag|Project) not found\z}
+ '' | :not_found | '404 Not Found'
+ '%20' | :bad_request | '"Package Name" not given'
end
with_them do
- it_behaves_like 'returning response status', params[:status]
+ it_behaves_like 'returning response status with error', status: params[:status], error: params[:error]
end
end
context 'with invalid tag name' do
- where(:tag_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
+ where(:tag_name, :status, :error) do
+ 'unknown' | :not_found | %r{\A(Package tag|Project) not found\z}
+ '' | :not_found | '404 Not Found'
+ '%20' | :bad_request | '"Tag" not given'
end
with_them do
- it_behaves_like 'returning response status', params[:status]
+ it_behaves_like 'returning response status with error', status: params[:status], error: params[:error]
end
end
end
diff --git a/spec/support/shared_examples/requests/response_status_with_error_shared_examples.rb b/spec/support/shared_examples/requests/response_status_with_error_shared_examples.rb
new file mode 100644
index 00000000000..de012aebaad
--- /dev/null
+++ b/spec/support/shared_examples/requests/response_status_with_error_shared_examples.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'returning response status with error' do |status:, error: nil|
+ it "returns #{status} and error message" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(json_response['error']).to be_present
+ expect(json_response['error']).to match(error) if error
+ end
+end
diff --git a/spec/support/shared_examples/services/auto_merge_shared_examples.rb b/spec/support/shared_examples/services/auto_merge_shared_examples.rb
new file mode 100644
index 00000000000..b295b65fdd1
--- /dev/null
+++ b/spec/support/shared_examples/services/auto_merge_shared_examples.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'for auto_merge strategy context' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:mr_merge_if_green_enabled) do
+ create(:merge_request,
+ merge_when_pipeline_succeeds: true,
+ merge_user: user,
+ source_branch: 'master', target_branch: 'feature',
+ source_project: project, target_project: project,
+ state: 'opened')
+ end
+
+ let(:pipeline) { create(:ci_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
+
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ before do
+ allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
+ end
+end
+
+RSpec.shared_examples 'auto_merge service #execute' do
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project,
+ source_branch: 'feature', target_branch: 'master')
+ end
+
+ context 'when first time enabling' do
+ before do
+ allow(merge_request)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+ allow(MailScheduler::NotificationServiceWorker).to receive(:perform_async)
+
+ service.execute(merge_request)
+ end
+
+ it 'sets the params, merge_user, and flag' do
+ expect(merge_request).to be_valid
+ expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
+ expect(merge_request.merge_params).to include 'commit_message' => 'Awesome message'
+ expect(merge_request.merge_user).to be user
+ expect(merge_request.auto_merge_strategy).to eq auto_merge_strategy
+ end
+
+ it 'schedules a notification' do
+ expect(MailScheduler::NotificationServiceWorker).to have_received(:perform_async).with(
+ 'merge_when_pipeline_succeeds', merge_request, user).once
+ end
+
+ it 'creates a system note' do
+ pipeline = build(:ci_pipeline)
+ allow(merge_request).to receive(:actual_head_pipeline) { pipeline }
+
+ note = merge_request.notes.last
+ expect(note.note).to match expected_note
+ end
+ end
+
+ context 'when already approved' do
+ let(:service) { described_class.new(project, user, should_remove_source_branch: true) }
+ let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
+
+ before do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+
+ allow(mr_merge_if_green_enabled).to receive(:mergeable?)
+ .and_return(true)
+
+ allow(pipeline).to receive(:success?).and_return(true)
+ end
+
+ it 'updates the merge params' do
+ expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds)
+ expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async).with(
+ 'merge_when_pipeline_succeeds', any_args)
+
+ service.execute(mr_merge_if_green_enabled)
+ expect(mr_merge_if_green_enabled.merge_params).to have_key('should_remove_source_branch')
+ end
+ end
+end
+
+RSpec.shared_examples 'auto_merge service #process' do
+ let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
+ let(:merge_request_head) do
+ project.commit(mr_merge_if_green_enabled.source_branch).id
+ end
+
+ context 'when triggered by pipeline with valid ref and sha' do
+ let(:triggering_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: merge_request_head, status: 'success',
+ head_pipeline_of: mr_merge_if_green_enabled)
+ end
+
+ it "merges all merge requests with merge when the pipeline succeeds enabled" do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: triggering_pipeline, actual_head_pipeline: triggering_pipeline)
+
+ expect(MergeWorker).to receive(:perform_async)
+ service.process(mr_merge_if_green_enabled)
+ end
+ end
+
+ context 'when triggered by an old pipeline' do
+ let(:old_pipeline) do
+ create(:ci_pipeline, project: project, ref: merge_request_ref,
+ sha: '1234abcdef', status: 'success')
+ end
+
+ it 'does not merge request' do
+ expect(MergeWorker).not_to receive(:perform_async)
+ service.process(mr_merge_if_green_enabled)
+ end
+ end
+
+ context 'when triggered by pipeline from a different branch' do
+ let(:unrelated_pipeline) do
+ create(:ci_pipeline, project: project, ref: 'feature',
+ sha: merge_request_head, status: 'success')
+ end
+
+ it 'does not merge request' do
+ expect(MergeWorker).not_to receive(:perform_async)
+ service.process(mr_merge_if_green_enabled)
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, :success,
+ source: :merge_request_event,
+ ref: mr_merge_if_green_enabled.merge_ref_path,
+ merge_request: mr_merge_if_green_enabled,
+ merge_requests_as_head_pipeline: [mr_merge_if_green_enabled])
+ end
+
+ it 'merges the associated merge request' do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+
+ expect(MergeWorker).to receive(:perform_async)
+ service.process(mr_merge_if_green_enabled)
+ end
+ end
+end
+
+RSpec.shared_examples 'auto_merge service #cancel' do
+ before do
+ service.cancel(mr_merge_if_green_enabled)
+ end
+
+ it "resets all the pipeline succeeds params" do
+ expect(mr_merge_if_green_enabled.merge_when_pipeline_succeeds).to be_falsey
+ expect(mr_merge_if_green_enabled.merge_params).to eq({})
+ expect(mr_merge_if_green_enabled.merge_user).to be nil
+ end
+
+ it 'posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'canceled the automatic merge'
+ end
+end
+
+RSpec.shared_examples 'auto_merge service #abort' do
+ before do
+ service.abort(mr_merge_if_green_enabled, 'an error')
+ end
+
+ it 'posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'aborted the automatic merge'
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
index 162be24fe8f..9d016e4830e 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
@@ -54,7 +54,7 @@ RSpec.shared_examples 'issues move service' do |group|
context 'when moving to backlog' do
let(:milestone) { create(:milestone, project: project) }
- let!(:backlog) { create(:backlog_list, board: board1) }
+ let!(:backlog) { board1.lists.backlog.first }
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression], milestone: milestone) }
let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: backlog.id } }
diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
index b9f28fab558..716ed482b4b 100644
--- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'lists list service' do
context 'when the board has a backlog list' do
- let!(:backlog_list) { create_backlog_list(board) }
+ let(:backlog_list) { board.lists.backlog.first }
it 'does not create a backlog list' do
expect { service.execute(board) }.not_to change { board.lists.count }
@@ -34,6 +34,10 @@ RSpec.shared_examples 'lists list service' do
end
context 'when the board does not have a backlog list' do
+ before do
+ board.lists.backlog.delete_all
+ end
+
it 'creates a backlog list' do
expect { service.execute(board) }.to change { board.lists.count }.by(1)
end
diff --git a/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb
index 5336e0f4c2f..8a3ab07bbfe 100644
--- a/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb
@@ -30,6 +30,7 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type|
context 'with a file generated by Gitlab CSV export' do
let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
+ let!(:test_milestone) { create(:milestone, project: project, title: 'v1.0') }
it 'imports the CSV without errors' do
expect(subject[:success]).to eq(4)
diff --git a/spec/support/shared_examples/views/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/views/nav_sidebar_shared_examples.rb
new file mode 100644
index 00000000000..d4c00738bdb
--- /dev/null
+++ b/spec/support/shared_examples/views/nav_sidebar_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'has nav sidebar' do
+ it 'has collapsed nav sidebar on mobile' do
+ render
+
+ expect(rendered).to have_selector('.nav-sidebar')
+ expect(rendered).not_to have_selector('.sidebar-collapsed-desktop')
+ expect(rendered).not_to have_selector('.sidebar-expanded-mobile')
+ end
+end
+
+RSpec.shared_examples 'sidebar includes snowplow attributes' do |track_action, track_label, track_property|
+ specify do
+ stub_application_setting(snowplow_enabled: true)
+
+ render
+
+ expect(rendered)
+ .to have_css(
+ ".nav-sidebar[data-track-action=\"#{track_action}\"]" \
+ "[data-track-label=\"#{track_label}\"][data-track-property=\"#{track_property}\"]"
+ )
+ end
+end
diff --git a/spec/support/shared_examples/views/pipeline_status_changes_email.rb b/spec/support/shared_examples/views/pipeline_status_changes_email.rb
index fe6cc5e03d2..a2db05c319e 100644
--- a/spec/support/shared_examples/views/pipeline_status_changes_email.rb
+++ b/spec/support/shared_examples/views/pipeline_status_changes_email.rb
@@ -25,6 +25,8 @@ RSpec.shared_examples 'pipeline status changes email' do
end
shared_examples_for 'renders the pipeline status changes email correctly' do
+ let(:pipeline_name_or_id) { pipeline.name || "##{pipeline.id}" }
+
context 'pipeline with user' do
it 'renders the email correctly' do
render
@@ -33,11 +35,12 @@ RSpec.shared_examples 'pipeline status changes email' do
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
expect(rendered).to have_content pipeline.commit.author_name
- expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content pipeline_name_or_id
expect(rendered).to have_content pipeline.user.name
if status == :failed
expect(rendered).to have_content build.name
+ expect(rendered).to include("#{build.project.full_path}/-/jobs/#{build.id}") unless build.is_a?(Ci::Bridge)
end
end
@@ -56,11 +59,12 @@ RSpec.shared_examples 'pipeline status changes email' do
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
expect(rendered).to have_content pipeline.commit.author_name
- expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content pipeline_name_or_id
expect(rendered).to have_content "by API"
if status == :failed
expect(rendered).to have_content build.name
+ expect(rendered).to include("#{build.project.full_path}/-/jobs/#{build.id}") unless build.is_a?(Ci::Bridge)
end
end
end
diff --git a/spec/support/shared_examples/views/preferred_language.rb b/spec/support/shared_examples/views/preferred_language.rb
new file mode 100644
index 00000000000..bd6c34bfcc7
--- /dev/null
+++ b/spec/support/shared_examples/views/preferred_language.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a layout which reflects the preferred language' do
+ context 'when changing the a preferred language' do
+ before do
+ Gitlab::I18n.locale = :es
+ end
+
+ after do
+ Gitlab::I18n.use_default_locale
+ end
+
+ it 'renders the correct `lang` attribute in the html element' do
+ render
+
+ expect(rendered).to have_css('html[lang=es]')
+ end
+ end
+end
diff --git a/spec/support/stub_dot_com_check.rb b/spec/support/stub_dot_com_check.rb
index 6934b33d111..53fa4e4e92e 100644
--- a/spec/support/stub_dot_com_check.rb
+++ b/spec/support/stub_dot_com_check.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.configure do |config|
- %i[saas saas_registration].each do |metadata|
+ %i[saas saas_registration saas_trial].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,
diff --git a/spec/support/system_exit_detected.rb b/spec/support/system_exit_detected.rb
index 86c6af3ba8c..62158e3877f 100644
--- a/spec/support/system_exit_detected.rb
+++ b/spec/support/system_exit_detected.rb
@@ -10,6 +10,9 @@ RSpec.configure do |config|
# because it'll skip any following tests from running.
# Convert it to something that won't skip everything.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/350060
+
+ raise if ENV['RSPEC_BYPASS_SYSTEM_EXIT_PROTECTION'] == 'true'
+
raise SystemExitDetected, "SystemExit should be rescued in the tests!"
end
end
diff --git a/spec/support/time_travel.rb b/spec/support/time_travel.rb
deleted file mode 100644
index 9dfbfd20524..00000000000
--- a/spec/support/time_travel.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'active_support/testing/time_helpers'
-
-RSpec.configure do |config|
- config.include ActiveSupport::Testing::TimeHelpers
-
- config.around(:example, :freeze_time) do |example|
- freeze_time { example.run }
- end
-
- config.around(:example, :time_travel_to) do |example|
- date_or_time = example.metadata[:time_travel_to]
-
- unless date_or_time.respond_to?(:to_time) && date_or_time.to_time.present?
- raise 'The time_travel_to RSpec metadata must have a Date or Time value.'
- end
-
- travel_to(date_or_time) { example.run }
- end
-end
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index 171c7ace2d2..1df92ce70cf 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -26,7 +26,14 @@ end
def allowed_host_and_ip(url)
host = URI.parse(url).host
ip_address = Addrinfo.ip(host).ip_address
- [host, ip_address]
+
+ allowed = [host, ip_address]
+
+ # Sometimes IPv6 address has square brackets around it
+ parsed_ip = IPAddr.new(ip_address)
+ allowed << "[#{ip_address}]" if parsed_ip.ipv6?
+
+ allowed
end
def with_net_connect_allowed
diff --git a/spec/support_specs/helpers/graphql_helpers_spec.rb b/spec/support_specs/helpers/graphql_helpers_spec.rb
index 12a6e561257..5d567bb7a13 100644
--- a/spec/support_specs/helpers/graphql_helpers_spec.rb
+++ b/spec/support_specs/helpers/graphql_helpers_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GraphqlHelpers do
- include GraphqlHelpers
+ include described_class
# Normalize irrelevant whitespace to make comparison easier
def norm(query)
diff --git a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
index 0e79e32b78a..19581064626 100644
--- a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
+++ b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
@@ -156,8 +156,8 @@ RSpec.describe ExceedQueryLimitHelpers do
expect(test_matcher.count_queries(recorder)).to eq({
'SELECT "schema_migrations".* FROM "schema_migrations"' => {
- %Q[WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1] => 2,
- %Q[WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1] => 1
+ %[WHERE "schema_migrations"."version" = 'foo\nbar\nbaz' LIMIT 1] => 2,
+ %[WHERE "schema_migrations"."version" = 'foo\nbiz\nbaz' LIMIT 1] => 1
}
})
end
diff --git a/spec/support_specs/matchers/result_matchers_spec.rb b/spec/support_specs/matchers/result_matchers_spec.rb
new file mode 100644
index 00000000000..0c30dd08009
--- /dev/null
+++ b/spec/support_specs/matchers/result_matchers_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../spec/support/matchers/result_matchers'
+
+RSpec.describe 'result matchers', feature_category: :remote_development do
+ include ResultMatchers
+
+ it 'works with value asserted via argument' do
+ expect(Result.ok(1)).to be_ok_result(1)
+ expect(Result.ok(1)).not_to be_ok_result(2)
+ expect(Result.ok(1)).not_to be_err_result(1)
+ end
+
+ it 'works with value asserted via block' do
+ expect(Result.err('hello')).to be_err_result do |result_value|
+ expect(result_value).to match(/hello/i)
+ end
+ end
+end
diff --git a/spec/support_specs/time_travel_spec.rb b/spec/support_specs/time_travel_spec.rb
deleted file mode 100644
index 8fa51c0c1f0..00000000000
--- a/spec/support_specs/time_travel_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'time travel' do
- describe ':freeze_time' do
- it 'freezes time around a spec example', :freeze_time do
- expect { sleep 0.1 }.not_to change { Time.now.to_f }
- end
- end
-
- describe ':time_travel_to' do
- it 'time-travels to the specified date', time_travel_to: '2020-01-01' do
- expect(Date.current).to eq(Date.new(2020, 1, 1))
- end
-
- it 'time-travels to the specified date & time', time_travel_to: '2020-02-02 10:30:45 -0700' do
- expect(Time.current).to eq(Time.new(2020, 2, 2, 17, 30, 45, '+00:00'))
- end
- end
-end
diff --git a/spec/tasks/dev_rake_spec.rb b/spec/tasks/dev_rake_spec.rb
index 82c9bb4faa2..f5490832982 100644
--- a/spec/tasks/dev_rake_spec.rb
+++ b/spec/tasks/dev_rake_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'dev rake tasks' do
end
def expect_connections_to_be_terminated
- expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection)
+ expect(Gitlab::Database::EachDatabase).to receive(:each_connection)
.with(include_shared: false)
.and_call_original
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 14bc6095b85..11c541ddfed 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -353,8 +353,8 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
describe 'schema inconsistencies' do
- let(:runner) { instance_double(Gitlab::Database::SchemaValidation::Runner, execute: inconsistencies) }
- let(:inconsistency_class) { Gitlab::Database::SchemaValidation::Inconsistency }
+ let(:runner) { instance_double(Gitlab::Schema::Validation::Runner, execute: inconsistencies) }
+ let(:inconsistency_class) { Gitlab::Schema::Validation::Inconsistency }
let(:inconsistencies) do
[
@@ -375,7 +375,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
before do
- allow(Gitlab::Database::SchemaValidation::Runner).to receive(:new).and_return(runner)
+ allow(Gitlab::Schema::Validation::Runner).to receive(:new).and_return(runner)
end
it 'prints the inconsistency message' do
@@ -1109,7 +1109,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
before do
each_database = class_double('Gitlab::Database::EachDatabase').as_stubbed_const
- allow(each_database).to receive(:each_database_connection)
+ allow(each_database).to receive(:each_connection)
.and_yield(connections[:main], 'main')
.and_yield(connections[:ci], 'ci')
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
index 33f4bca4c85..f495c7e8911 100644
--- a/spec/tasks/gitlab/feature_categories_rake_spec.rb
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -40,7 +40,8 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
)
),
'database_tables' => a_hash_including(
- 'container_scanning' => a_collection_including('vulnerability_advisories')
+ 'continuous_integration' => a_collection_including('ci_pipelines'),
+ 'user_profile' => a_collection_including('users')
)
}
diff --git a/spec/tasks/gitlab/metrics_exporter_rake_spec.rb b/spec/tasks/gitlab/metrics_exporter_rake_spec.rb
deleted file mode 100644
index ca37fc1b5d7..00000000000
--- a/spec/tasks/gitlab/metrics_exporter_rake_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'rake_helper'
-require_relative '../../support/helpers/next_instance_of'
-
-RSpec.describe 'gitlab:metrics_exporter:install', feature_category: :metrics do
- before do
- Rake.application.rake_require 'tasks/gitlab/metrics_exporter'
- end
-
- subject(:task) do
- Rake::Task['gitlab:metrics_exporter:install']
- end
-
- context 'when no target directory is specified' do
- it 'aborts with an error message' do
- expect do
- expect { task.execute }.to output(/Please specify the directory/).to_stdout
- end.to raise_error(SystemExit)
- end
- end
-
- context 'when target directory is specified' do
- let(:args) { Rake::TaskArguments.new(%w[dir], %w[path/to/exporter]) }
- let(:context) { TOPLEVEL_BINDING.eval('self') }
- let(:expected_clone_params) do
- {
- repo: 'https://gitlab.com/gitlab-org/gitlab-metrics-exporter.git',
- version: an_instance_of(String),
- target_dir: 'path/to/exporter'
- }
- end
-
- context 'when dependencies are missing' do
- it 'aborts with an error message' do
- expect(Gitlab::Utils).to receive(:which).with('gmake').ordered
- expect(Gitlab::Utils).to receive(:which).with('make').ordered
-
- expect do
- expect { task.execute(args) }.to output(/Couldn't find a 'make' binary/).to_stdout
- end.to raise_error(SystemExit)
- end
- end
-
- it 'installs the exporter with gmake' do
- expect(Gitlab::Utils).to receive(:which).with('gmake').and_return('path/to/gmake').ordered
- expect(context).to receive(:checkout_or_clone_version).with(hash_including(expected_clone_params)).ordered
- expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered
- expect(context).to receive(:run_command!).with(['path/to/gmake']).ordered
-
- task.execute(args)
- end
-
- it 'installs the exporter with make' do
- expect(Gitlab::Utils).to receive(:which).with('gmake').ordered
- expect(Gitlab::Utils).to receive(:which).with('make').and_return('path/to/make').ordered
- expect(context).to receive(:checkout_or_clone_version).with(hash_including(expected_clone_params)).ordered
- expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered
- expect(context).to receive(:run_command!).with(['path/to/make']).ordered
-
- task.execute(args)
- end
-
- context 'when overriding version via environment variable' do
- before do
- stub_env('GITLAB_METRICS_EXPORTER_VERSION', '1.0')
- end
-
- it 'clones from repository with that version instead' do
- expect(Gitlab::Utils).to receive(:which).with('gmake').and_return('path/to/gmake').ordered
- expect(context).to receive(:checkout_or_clone_version).with(
- hash_including(expected_clone_params.merge(version: '1.0'))
- ).ordered
- expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered
- expect(context).to receive(:run_command!).with(['path/to/gmake']).ordered
-
- task.execute(args)
- end
- end
- end
-end
diff --git a/spec/tasks/gitlab/packages/events_rake_spec.rb b/spec/tasks/gitlab/packages/events_rake_spec.rb
deleted file mode 100644
index 87f4db360ca..00000000000
--- a/spec/tasks/gitlab/packages/events_rake_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'rake_helper'
-
-RSpec.describe 'gitlab:packages:events namespace rake task', :silence_stdout do
- before :all do
- Rake.application.rake_require 'tasks/gitlab/packages/events'
- end
-
- subject do
- file = double('file')
- yml_file = nil
-
- allow(file).to receive(:<<) { |contents| yml_file = contents }
- allow(File).to receive(:open).and_yield(file)
-
- run_rake_task("gitlab:packages:events:#{task}")
-
- YAML.safe_load(yml_file)
- end
-
- describe 'generate_unique' do
- let(:task) { 'generate_unique' }
-
- it 'excludes guest events' do
- expect(subject.find { |event| event['name'].include?("guest") }).to be_nil
- end
-
- Packages::Event::EVENT_SCOPES.keys.each do |event_scope|
- it "includes `#{event_scope}` scope" do
- expect(subject.find { |event| event['name'].include?(event_scope) }).not_to be_nil
- end
- end
-
- it 'excludes some event types' do
- expect(subject.grep(/search_package/)).to be_empty
- expect(subject.grep(/list_package/)).to be_empty
- end
- end
-
- describe 'generate_counts' do
- let(:task) { 'generate_counts' }
-
- Packages::Event::EVENT_SCOPES.keys.each do |event_scope|
- it "includes `#{event_scope}` scope" do
- expect(subject.find { |event| event.include?(event_scope) }).not_to be_nil
- end
- end
-
- it 'excludes some event types' do
- expect(subject.find { |event| event.include?("search_package") }).to be_nil
- expect(subject.find { |event| event.include?("list_package") }).to be_nil
- end
- end
-end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 195859eac70..30f512205f9 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -24,21 +24,75 @@ RSpec.describe 'gitlab:shell rake tasks', :silence_stdout do
end
describe 'setup task' do
- it 'writes authorized keys into the file' do
- allow(Gitlab::CurrentSettings).to receive(:authorized_keys_enabled?).and_return(true)
- stub_env('force', 'yes')
+ let!(:auth_key) { create(:key) }
+ let!(:auth_and_signing_key) { create(:key, usage_type: :auth_and_signing) }
- auth_key = create(:key)
- auth_and_signing_key = create(:key, usage_type: :auth_and_signing)
+ before do
create(:key, usage_type: :signing)
- expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance|
- expect(instance).to receive(:batch_add_keys).once do |keys|
- expect(keys).to match_array([auth_key, auth_and_signing_key])
+ allow(Gitlab::CurrentSettings).to receive(:authorized_keys_enabled?).and_return(write_to_authorized_keys)
+ end
+
+ context 'when "Write to authorized keys" is enabled' do
+ let(:write_to_authorized_keys) { true }
+
+ before do
+ stub_env('force', force)
+ end
+
+ context 'when "force" is not set' do
+ let(:force) { nil }
+
+ context 'when the user answers "yes"' do
+ it 'writes authorized keys into the file' do
+ allow(main_object).to receive(:ask_to_continue)
+
+ expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance|
+ expect(instance).to receive(:batch_add_keys).once do |keys|
+ expect(keys).to match_array([auth_key, auth_and_signing_key])
+ end
+ end
+
+ run_rake_task('gitlab:shell:setup')
+ end
+ end
+
+ context 'when the user answers "no"' do
+ it 'does not write authorized keys into the file' do
+ allow(main_object).to receive(:ask_to_continue).and_raise(Gitlab::TaskAbortedByUserError)
+
+ expect(Gitlab::AuthorizedKeys).not_to receive(:new)
+
+ expect do
+ run_rake_task('gitlab:shell:setup')
+ end.to raise_error(SystemExit)
+ end
+ end
+ end
+
+ context 'when "force" is set to "yes"' do
+ let(:force) { 'yes' }
+
+ it 'writes authorized keys into the file' do
+ expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance|
+ expect(instance).to receive(:batch_add_keys).once do |keys|
+ expect(keys).to match_array([auth_key, auth_and_signing_key])
+ end
+ end
+
+ run_rake_task('gitlab:shell:setup')
end
end
+ end
+
+ context 'when "Write to authorized keys" is disabled' do
+ let(:write_to_authorized_keys) { false }
- run_rake_task('gitlab:shell:setup')
+ it 'does not write authorized keys into the file' do
+ expect(Gitlab::AuthorizedKeys).not_to receive(:new)
+
+ run_rake_task('gitlab:shell:setup')
+ end
end
end
end
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
index 11aab1b1b42..170b1319154 100644
--- a/spec/tasks/gitlab/usage_data_rake_spec.rb
+++ b/spec/tasks/gitlab/usage_data_rake_spec.rb
@@ -69,23 +69,6 @@ RSpec.describe 'gitlab:usage data take tasks', :silence_stdout, :with_license, f
expect { run_rake_task('gitlab:usage_data:generate_and_send') }.to output(/.*201.*/).to_stdout
end
- describe 'generate_ci_template_events' do
- around do |example|
- FileUtils.rm_rf(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH)
-
- example.run
-
- `git checkout -- #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}`
- end
-
- it "generates #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}",
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/386191' do
- run_rake_task('gitlab:usage_data:generate_ci_template_events')
-
- expect(File.exist?(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH)).to be true
- end
- end
-
private
def stub_response(body:, url: service_ping_payload_url, status: 201)
diff --git a/spec/tooling/danger/experiments_spec.rb b/spec/tooling/danger/experiments_spec.rb
new file mode 100644
index 00000000000..85f8060a3ec
--- /dev/null
+++ b/spec/tooling/danger/experiments_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'gitlab-dangerfiles'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/experiments'
+
+RSpec.describe Tooling::Danger::Experiments, feature_category: :tooling do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+
+ subject(:experiments) { fake_danger.new(helper: fake_helper) }
+
+ describe '#removed_experiments' do
+ let(:removed_experiments_yml_files) do
+ [
+ 'config/feature_flags/experiment/tier_badge.yml',
+ 'ee/config/feature_flags/experiment/direct_to_trial.yml'
+ ]
+ end
+
+ let(:deleted_files) do
+ [
+ 'app/models/model.rb',
+ 'app/assets/javascripts/file.js'
+ ] + removed_experiments_yml_files
+ end
+
+ it 'returns names of removed experiments' do
+ expect(experiments.removed_experiments).to eq(%w[tier_badge direct_to_trial])
+ end
+ end
+
+ describe '#class_files_removed?' do
+ let(:removed_experiments_name) { current_experiment_with_class_files_example }
+
+ context 'when yml file is deleted but not class file' do
+ let(:deleted_files) { ["config/feature_flags/experiment/#{removed_experiments_name}.yml"] }
+
+ it 'returns false' do
+ expect(experiments.class_files_removed?).to eq(false)
+ end
+ end
+
+ context 'when yml file is deleted but no corresponding class file exists' do
+ let(:deleted_files) { ["config/feature_flags/experiment/fake_experiment.yml"] }
+
+ it 'returns true' do
+ expect(experiments.class_files_removed?).to eq(true)
+ end
+ end
+ end
+
+ def current_experiment_with_class_files_example
+ path = Dir.glob("app/experiments/*.rb").last
+ File.basename(path).chomp('_experiment.rb')
+ end
+end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 3910f569400..5ae0a8695eb 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -5,9 +5,9 @@ require 'gitlab-dangerfiles'
require 'danger'
require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
+require 'gitlab/rspec/all'
require_relative '../../../danger/plugins/project_helper'
-require_relative '../../../spec/support/helpers/stub_env'
RSpec.describe Tooling::Danger::ProjectHelper do
include StubENV
@@ -65,6 +65,11 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'config/foo.js' | [:frontend]
'config/deep/foo.js' | [:frontend]
+ 'app/components/pajamas/empty_state_component.html.haml' | [:frontend, :backend]
+ 'ee/app/components/pajamas/empty_state_component.html.haml' | [:frontend, :backend]
+ 'app/components/diffs/overflow_warning_component.html.haml' | [:frontend, :backend]
+ 'app/components/layouts/horizontal_section_component.rb' | [:frontend, :backend]
+
'ee/app/assets/foo' | [:frontend]
'ee/app/views/foo' | [:frontend, :backend]
'ee/spec/frontend/bar' | [:frontend]
@@ -80,6 +85,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'.rubocop.yml' | [:backend]
'.rubocop_todo.yml' | [:backend]
'.rubocop_todo/cop/name.yml' | [:backend]
+ 'gems/foo/.rubocop.yml' | [:backend]
'spec/foo' | [:backend]
'spec/foo/bar' | [:backend]
@@ -112,7 +118,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'scripts/glfm/bar.rb' | [:backend]
'scripts/glfm/bar.js' | [:frontend]
- 'scripts/remote_development/run-smoke-test-suite.sh' | [:remote_development]
+ 'scripts/remote_development/run-smoke-test-suite.sh' | [:remote_development_be]
'scripts/lib/glfm/bar.rb' | [:backend]
'scripts/lib/glfm/bar.js' | [:frontend]
'scripts/bar.rb' | [:backend, :tooling]
@@ -136,6 +142,8 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'tooling/bin/find_foss_tests' | [:tooling]
'.codeclimate.yml' | [:tooling]
'.gitlab/CODEOWNERS' | [:tooling]
+ 'gems/gem.gitlab-ci.yml' | [:tooling]
+ 'gems/config/rubocop.yml' | [:tooling]
'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template]
'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template]
diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb
index 43c3da5699d..fef29ad3f2c 100644
--- a/spec/tooling/lib/tooling/find_changes_spec.rb
+++ b/spec/tooling/lib/tooling/find_changes_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative '../../../../tooling/lib/tooling/find_changes'
-require_relative '../../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
require 'json'
require 'tempfile'
diff --git a/spec/tooling/lib/tooling/find_tests_spec.rb b/spec/tooling/lib/tooling/find_tests_spec.rb
index 905f81c4bbd..67b6650b335 100644
--- a/spec/tooling/lib/tooling/find_tests_spec.rb
+++ b/spec/tooling/lib/tooling/find_tests_spec.rb
@@ -2,7 +2,7 @@
require 'tempfile'
require_relative '../../../../tooling/lib/tooling/find_tests'
-require_relative '../../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
RSpec.describe Tooling::FindTests, feature_category: :tooling do
include StubENV
diff --git a/spec/tooling/lib/tooling/gettext_extractor_spec.rb b/spec/tooling/lib/tooling/gettext_extractor_spec.rb
index 3c0f91342c2..14310c804f1 100644
--- a/spec/tooling/lib/tooling/gettext_extractor_spec.rb
+++ b/spec/tooling/lib/tooling/gettext_extractor_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'rspec/parameterized'
+require 'gitlab/rspec/all'
require_relative '../../../../tooling/lib/tooling/gettext_extractor'
-require_relative '../../../support/helpers/stub_env'
require_relative '../../../support/tmpdir'
RSpec.describe Tooling::GettextExtractor, feature_category: :tooling do
diff --git a/spec/tooling/lib/tooling/predictive_tests_spec.rb b/spec/tooling/lib/tooling/predictive_tests_spec.rb
index b82364fe6f6..fdb7d09a3e2 100644
--- a/spec/tooling/lib/tooling/predictive_tests_spec.rb
+++ b/spec/tooling/lib/tooling/predictive_tests_spec.rb
@@ -2,8 +2,8 @@
require 'tempfile'
require 'fileutils'
+require 'gitlab/rspec/all'
require_relative '../../../../tooling/lib/tooling/predictive_tests'
-require_relative '../../../support/helpers/stub_env'
RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do
include StubENV
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index a7e4e42206a..6ccd2e46f7b 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -238,7 +238,7 @@ RSpec.describe Quality::TestLevel, feature_category: :tooling do
it 'ensures all spec/ folders are covered by a test level' do
Dir['{,ee/}spec/**/*/'].each do |path|
- next if path =~ %r{\A(ee/)?spec/(benchmarks|docs_screenshots|fixtures|frontend_integration|support)/}
+ next if %r{\A(ee/)?spec/(benchmarks|docs_screenshots|fixtures|frontend_integration|support)/}.match?(path)
expect { subject.level_for(path) }.not_to raise_error
end
diff --git a/spec/tooling/rspec_flaky/config_spec.rb b/spec/tooling/rspec_flaky/config_spec.rb
deleted file mode 100644
index 63f42d7c6cc..00000000000
--- a/spec/tooling/rspec_flaky/config_spec.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-# frozen_string_literal: true
-
-require 'rspec-parameterized'
-require_relative '../../support/helpers/stub_env'
-
-require_relative '../../../tooling/rspec_flaky/config'
-
-RSpec.describe RspecFlaky::Config, :aggregate_failures do
- include StubENV
-
- before do
- # Stub these env variables otherwise specs don't behave the same on the CI
- stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil)
- stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
- stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
- stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
- # Ensure the behavior is the same locally and on CI (where Rails is defined since we run this test as part of the whole suite), i.e. Rails isn't defined
- allow(described_class).to receive(:rails_path).and_wrap_original do |method, path|
- path
- end
- end
-
- describe '.generate_report?' do
- context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is not set" do
- it 'returns false' do
- expect(described_class).not_to be_generate_report
- end
- end
-
- context "when ENV['FLAKY_RSPEC_GENERATE_REPORT'] is set" do
- using RSpec::Parameterized::TableSyntax
-
- where(:env_value, :result) do
- '1' | true
- 'true' | true
- 'foo' | false
- '0' | false
- 'false' | false
- end
-
- with_them do
- before do
- stub_env('FLAKY_RSPEC_GENERATE_REPORT', env_value)
- end
-
- it 'returns false' do
- expect(described_class.generate_report?).to be(result)
- end
- end
- end
- end
-
- describe '.suite_flaky_examples_report_path' do
- context "when ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] is not set" do
- it 'returns the default path' do
- expect(described_class.suite_flaky_examples_report_path).to eq('rspec/flaky/suite-report.json')
- end
- end
-
- context "when ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] is set" do
- before do
- stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', 'foo/suite-report.json')
- end
-
- it 'returns the value of the env variable' do
- expect(described_class.suite_flaky_examples_report_path).to eq('foo/suite-report.json')
- end
- end
- end
-
- describe '.flaky_examples_report_path' do
- context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do
- it 'returns the default path' do
- expect(described_class.flaky_examples_report_path).to eq('rspec/flaky/report.json')
- end
- end
-
- context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is set" do
- before do
- stub_env('FLAKY_RSPEC_REPORT_PATH', 'foo/report.json')
- end
-
- it 'returns the value of the env variable' do
- expect(described_class.flaky_examples_report_path).to eq('foo/report.json')
- end
- end
- end
-
- describe '.new_flaky_examples_report_path' do
- context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do
- it 'returns the default path' do
- expect(described_class.new_flaky_examples_report_path).to eq('rspec/flaky/new-report.json')
- end
- end
-
- context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is set" do
- before do
- stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', 'foo/new-report.json')
- end
-
- it 'returns the value of the env variable' do
- expect(described_class.new_flaky_examples_report_path).to eq('foo/new-report.json')
- end
- end
- end
-end
diff --git a/spec/tooling/rspec_flaky/example_spec.rb b/spec/tooling/rspec_flaky/example_spec.rb
deleted file mode 100644
index d001ed32444..00000000000
--- a/spec/tooling/rspec_flaky/example_spec.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../tooling/rspec_flaky/example'
-
-RSpec.describe RspecFlaky::Example do
- let(:example_attrs) do
- {
- id: 'spec/foo/bar_spec.rb:2',
- metadata: {
- file_path: 'spec/foo/bar_spec.rb',
- line_number: 2,
- full_description: 'hello world',
- feature_category: :feature_category
- },
- execution_result: double(status: 'passed', exception: 'BOOM!'),
- attempts: 1
- }
- end
-
- let(:rspec_example) { double(example_attrs) }
-
- describe '#initialize' do
- shared_examples 'a valid Example instance' do
- it 'returns valid attributes' do
- example = described_class.new(args)
-
- expect(example.example_id).to eq(example_attrs[:id])
- end
- end
-
- context 'when given an Rspec::Core::Example that responds to #example' do
- let(:args) { double(example: rspec_example) }
-
- it_behaves_like 'a valid Example instance'
- end
-
- context 'when given an Rspec::Core::Example that does not respond to #example' do
- let(:args) { rspec_example }
-
- it_behaves_like 'a valid Example instance'
- end
- end
-
- subject { described_class.new(rspec_example) }
-
- describe '#uid' do
- it 'returns a hash of the full description' do
- expect(subject.uid).to eq(Digest::MD5.hexdigest("#{subject.description}-#{subject.file}"))
- end
- end
-
- describe '#example_id' do
- it 'returns the ID of the RSpec::Core::Example' do
- expect(subject.example_id).to eq(rspec_example.id)
- end
- end
-
- describe '#attempts' do
- it 'returns the attempts of the RSpec::Core::Example' do
- expect(subject.attempts).to eq(rspec_example.attempts)
- end
- end
-
- describe '#file' do
- it 'returns the metadata[:file_path] of the RSpec::Core::Example' do
- expect(subject.file).to eq(rspec_example.metadata[:file_path])
- end
- end
-
- describe '#line' do
- it 'returns the metadata[:line_number] of the RSpec::Core::Example' do
- expect(subject.line).to eq(rspec_example.metadata[:line_number])
- end
- end
-
- describe '#description' do
- it 'returns the metadata[:full_description] of the RSpec::Core::Example' do
- expect(subject.description).to eq(rspec_example.metadata[:full_description])
- end
- end
-
- describe '#status' do
- it 'returns the execution_result.status of the RSpec::Core::Example' do
- expect(subject.status).to eq(rspec_example.execution_result.status)
- end
- end
-
- describe '#exception' do
- it 'returns the execution_result.exception of the RSpec::Core::Example' do
- expect(subject.exception).to eq(rspec_example.execution_result.exception)
- end
- end
-
- describe '#feature_category' do
- it 'returns the metadata[:feature_category] of the RSpec::Core::Example' do
- expect(subject.feature_category).to eq(rspec_example.metadata[:feature_category])
- end
- end
-end
diff --git a/spec/tooling/rspec_flaky/flaky_example_spec.rb b/spec/tooling/rspec_flaky/flaky_example_spec.rb
deleted file mode 100644
index 511f3286f56..00000000000
--- a/spec/tooling/rspec_flaky/flaky_example_spec.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../support/helpers/stub_env'
-require_relative '../../support/time_travel'
-
-require_relative '../../../tooling/rspec_flaky/flaky_example'
-
-RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
- include ActiveSupport::Testing::TimeHelpers
- include StubENV
-
- let(:example_attrs) do
- {
- example_id: 'spec/foo/bar_spec.rb:2',
- file: 'spec/foo/bar_spec.rb',
- line: 2,
- description: 'hello world',
- last_attempts_count: 2,
- feature_category: :feature_category
- }
- end
-
- before do
- # Stub these env variables otherwise specs don't behave the same on the CI
- stub_env('CI_JOB_URL', nil)
- end
-
- describe '#initialize', :freeze_time do
- shared_examples 'a valid FlakyExample instance' do
- let(:flaky_example) { described_class.new(args) }
-
- it 'returns valid attributes' do
- attrs = flaky_example.to_h
-
- expect(attrs[:uid]).to eq(example_attrs[:uid])
- expect(attrs[:file]).to eq(example_attrs[:file])
- expect(attrs[:line]).to eq(example_attrs[:line])
- expect(attrs[:description]).to eq(example_attrs[:description])
- expect(attrs[:feature_category]).to eq(example_attrs[:feature_category])
- expect(attrs[:first_flaky_at]).to eq(expected_first_flaky_at)
- expect(attrs[:last_flaky_at]).to eq(expected_last_flaky_at)
- expect(attrs[:last_attempts_count]).to eq(example_attrs[:last_attempts_count])
- expect(attrs[:flaky_reports]).to eq(expected_flaky_reports)
- end
- end
-
- context 'when given an Example.to_h' do
- it_behaves_like 'a valid FlakyExample instance' do
- let(:args) { example_attrs }
- let(:expected_first_flaky_at) { Time.now }
- let(:expected_last_flaky_at) { Time.now }
- let(:expected_flaky_reports) { 0 }
- end
- end
- end
-
- describe '#update!' do
- shared_examples 'an up-to-date FlakyExample instance' do
- let(:flaky_example) { described_class.new(args) }
-
- it 'sets the first_flaky_at if none exists' do
- args[:first_flaky_at] = nil
-
- freeze_time do
- flaky_example.update!(example_attrs)
-
- expect(flaky_example.to_h[:first_flaky_at]).to eq(Time.now)
- end
- end
-
- it 'maintains the first_flaky_at if exists' do
- flaky_example.update!(example_attrs)
- expected_first_flaky_at = flaky_example.to_h[:first_flaky_at]
-
- travel_to(Time.now + 42) do
- flaky_example.update!(example_attrs)
- expect(flaky_example.to_h[:first_flaky_at]).to eq(expected_first_flaky_at)
- end
- end
-
- it 'updates the last_flaky_at' do
- travel_to(Time.now + 42) do
- the_future = Time.now
- flaky_example.update!(example_attrs)
-
- expect(flaky_example.to_h[:last_flaky_at]).to eq(the_future)
- end
- end
-
- it 'updates the flaky_reports' do
- expected_flaky_reports = flaky_example.to_h[:first_flaky_at] ? flaky_example.to_h[:flaky_reports] + 1 : 1
-
- expect { flaky_example.update!(example_attrs) }.to change { flaky_example.to_h[:flaky_reports] }.by(1)
- expect(flaky_example.to_h[:flaky_reports]).to eq(expected_flaky_reports)
- end
-
- it 'updates the last_attempts_count' do
- example_attrs[:last_attempts_count] = 42
- flaky_example.update!(example_attrs)
-
- expect(flaky_example.to_h[:last_attempts_count]).to eq(42)
- end
-
- context 'when run on the CI' do
- let(:job_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/42' }
-
- before do
- stub_env('CI_JOB_URL', job_url)
- end
-
- it 'updates the last_flaky_job' do
- flaky_example.update!(example_attrs)
-
- expect(flaky_example.to_h[:last_flaky_job]).to eq(job_url)
- end
- end
- end
-
- context 'when given an Example hash' do
- it_behaves_like 'an up-to-date FlakyExample instance' do
- let(:args) { example_attrs }
- end
- end
- end
-
- describe '#to_h', :freeze_time do
- shared_examples 'a valid FlakyExample hash' do
- let(:additional_attrs) { {} }
-
- it 'returns a valid hash' do
- flaky_example = described_class.new(args)
- final_hash = example_attrs.merge(additional_attrs)
-
- expect(flaky_example.to_h).to eq(final_hash)
- end
- end
-
- context 'when given an Example hash' do
- let(:args) { example_attrs }
-
- it_behaves_like 'a valid FlakyExample hash' do
- let(:additional_attrs) do
- { first_flaky_at: Time.now, last_flaky_at: Time.now, last_flaky_job: nil, flaky_reports: 0 }
- end
- end
- end
- end
-end
diff --git a/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb b/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb
deleted file mode 100644
index 9d75c97febe..00000000000
--- a/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../support/time_travel'
-
-require_relative '../../../tooling/rspec_flaky/flaky_examples_collection'
-
-RSpec.describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures, :freeze_time do
- let(:collection_hash) do
- {
- a: { example_id: 'spec/foo/bar_spec.rb:2' },
- b: { example_id: 'spec/foo/baz_spec.rb:3' }
- }
- end
-
- let(:collection_report) do
- {
- a: {
- example_id: 'spec/foo/bar_spec.rb:2',
- first_flaky_at: Time.now,
- last_flaky_at: Time.now,
- last_flaky_job: nil,
- flaky_reports: 0,
- feature_category: nil,
- last_attempts_count: nil
- },
- b: {
- example_id: 'spec/foo/baz_spec.rb:3',
- first_flaky_at: Time.now,
- last_flaky_at: Time.now,
- last_flaky_job: nil,
- flaky_reports: 0,
- feature_category: nil,
- last_attempts_count: nil
- }
- }
- end
-
- describe '#initialize' do
- it 'accepts no argument' do
- expect { described_class.new }.not_to raise_error
- end
-
- it 'accepts a hash' do
- expect { described_class.new(collection_hash) }.not_to raise_error
- end
-
- it 'does not accept anything else' do
- expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`collection` must be a Hash, Array given!")
- end
- end
-
- describe '#to_h' do
- it 'calls #to_h on the values' do
- collection = described_class.new(collection_hash)
-
- expect(collection.to_h).to eq(collection_report)
- end
- end
-
- describe '#-' do
- it 'returns only examples that are not present in the given collection' do
- collection1 = described_class.new(collection_hash)
- collection2 = described_class.new(
- a: { example_id: 'spec/foo/bar_spec.rb:2' },
- c: { example_id: 'spec/bar/baz_spec.rb:4' })
-
- expect((collection2 - collection1).to_h).to eq(
- c: {
- example_id: 'spec/bar/baz_spec.rb:4',
- first_flaky_at: Time.now,
- last_flaky_at: Time.now,
- last_flaky_job: nil,
- flaky_reports: 0,
- feature_category: nil,
- last_attempts_count: nil
- })
- end
-
- it 'fails if the given collection does not respond to `#key?`' do
- collection = described_class.new(collection_hash)
-
- expect { collection - [1, 2, 3] }.to raise_error(ArgumentError, "`other` must respond to `#key?`, Array does not!")
- end
- end
-end
diff --git a/spec/tooling/rspec_flaky/listener_spec.rb b/spec/tooling/rspec_flaky/listener_spec.rb
deleted file mode 100644
index 0bbd6454969..00000000000
--- a/spec/tooling/rspec_flaky/listener_spec.rb
+++ /dev/null
@@ -1,228 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../support/helpers/stub_env'
-require_relative '../../support/time_travel'
-
-require_relative '../../../tooling/rspec_flaky/listener'
-
-RSpec.describe RspecFlaky::Listener, :aggregate_failures do
- include ActiveSupport::Testing::TimeHelpers
- include StubENV
-
- let(:already_flaky_example_uid) { '6e869794f4cfd2badd93eb68719371d1' }
- let(:suite_flaky_example_report) do
- {
- "#{already_flaky_example_uid}": {
- example_id: 'spec/foo/bar_spec.rb:2',
- file: 'spec/foo/bar_spec.rb',
- line: 2,
- description: 'hello world',
- first_flaky_at: 1234,
- last_flaky_at: 4321,
- last_attempts_count: 3,
- flaky_reports: 1,
- last_flaky_job: nil
- }
- }
- end
-
- let(:already_flaky_example_attrs) do
- {
- id: 'spec/foo/bar_spec.rb:2',
- metadata: {
- file_path: 'spec/foo/bar_spec.rb',
- line_number: 2,
- full_description: 'hello world'
- },
- execution_result: double(status: 'passed', exception: nil)
- }
- end
-
- let(:already_flaky_example) { RspecFlaky::FlakyExample.new(suite_flaky_example_report[already_flaky_example_uid]) }
- let(:new_example_attrs) do
- {
- id: 'spec/foo/baz_spec.rb:3',
- metadata: {
- file_path: 'spec/foo/baz_spec.rb',
- line_number: 3,
- full_description: 'hello GitLab'
- },
- execution_result: double(status: 'passed', exception: nil)
- }
- end
-
- before do
- # Stub these env variables otherwise specs don't behave the same on the CI
- stub_env('CI_JOB_URL', nil)
- stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
- end
-
- describe '#initialize' do
- shared_examples 'a valid Listener instance' do
- let(:expected_suite_flaky_examples) { {} }
-
- it 'returns a valid Listener instance' do
- listener = described_class.new
-
- expect(listener.suite_flaky_examples.to_h).to eq(expected_suite_flaky_examples)
- expect(listener.flaky_examples).to eq({})
- end
- end
-
- context 'when no report file exists' do
- it_behaves_like 'a valid Listener instance'
- end
-
- context 'when FLAKY_RSPEC_SUITE_REPORT_PATH is set' do
- let(:report_file_path) { 'foo/report.json' }
-
- before do
- stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', report_file_path)
- end
-
- context 'and report file exists' do
- before do
- expect(File).to receive(:exist?).with(report_file_path).and_return(true)
- end
-
- it 'delegates the load to RspecFlaky::Report' do
- report = RspecFlaky::Report.new(RspecFlaky::FlakyExamplesCollection.new(suite_flaky_example_report))
-
- expect(RspecFlaky::Report).to receive(:load).with(report_file_path).and_return(report)
- expect(described_class.new.suite_flaky_examples.to_h).to eq(report.flaky_examples.to_h)
- end
- end
-
- context 'and report file does not exist' do
- before do
- expect(File).to receive(:exist?).with(report_file_path).and_return(false)
- end
-
- it 'return an empty hash' do
- expect(RspecFlaky::Report).not_to receive(:load)
- expect(described_class.new.suite_flaky_examples.to_h).to eq({})
- end
- end
- end
- end
-
- describe '#example_passed' do
- let(:rspec_example) { double(new_example_attrs) }
- let(:notification) { double(example: rspec_example) }
- let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
-
- shared_examples 'a non-flaky example' do
- it 'does not change the flaky examples hash' do
- expect { listener.example_passed(notification) }
- .not_to change { listener.flaky_examples }
- end
- end
-
- shared_examples 'an existing flaky example' do
- let(:expected_flaky_example) do
- {
- example_id: 'spec/foo/bar_spec.rb:2',
- file: 'spec/foo/bar_spec.rb',
- line: 2,
- description: 'hello world',
- first_flaky_at: 1234,
- last_attempts_count: 2,
- flaky_reports: 2,
- feature_category: nil,
- last_flaky_job: nil
- }
- end
-
- it 'changes the flaky examples hash' do
- new_example = RspecFlaky::Example.new(rspec_example)
-
- travel_to(Time.now + 42) do
- the_future = Time.now
- expect { listener.example_passed(notification) }
- .to change { listener.flaky_examples[new_example.uid].to_h }
- expect(listener.flaky_examples[new_example.uid].to_h)
- .to eq(expected_flaky_example.merge(last_flaky_at: the_future))
- end
- end
- end
-
- shared_examples 'a new flaky example' do
- let(:expected_flaky_example) do
- {
- example_id: 'spec/foo/baz_spec.rb:3',
- file: 'spec/foo/baz_spec.rb',
- line: 3,
- description: 'hello GitLab',
- last_attempts_count: 2,
- flaky_reports: 1,
- feature_category: nil,
- last_flaky_job: nil
- }
- end
-
- it 'changes the all flaky examples hash' do
- new_example = RspecFlaky::Example.new(rspec_example)
-
- travel_to(Time.now + 42) do
- the_future = Time.now
- expect { listener.example_passed(notification) }
- .to change { listener.flaky_examples[new_example.uid].to_h }
- expect(listener.flaky_examples[new_example.uid].to_h)
- .to eq(expected_flaky_example.merge(first_flaky_at: the_future, last_flaky_at: the_future))
- end
- end
- end
-
- describe 'when the RSpec example does not respond to attempts' do
- it_behaves_like 'a non-flaky example'
- end
-
- describe 'when the RSpec example has 1 attempt' do
- let(:rspec_example) { double(new_example_attrs.merge(attempts: 1)) }
-
- it_behaves_like 'a non-flaky example'
- end
-
- describe 'when the RSpec example has 2 attempts' do
- let(:rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
-
- it_behaves_like 'a new flaky example'
-
- context 'with an existing flaky example' do
- let(:rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
-
- it_behaves_like 'an existing flaky example'
- end
- end
- end
-
- describe '#dump_summary' do
- let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
- let(:new_flaky_rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
- let(:already_flaky_rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
- let(:notification_new_flaky_rspec_example) { double(example: new_flaky_rspec_example) }
- let(:notification_already_flaky_rspec_example) { double(example: already_flaky_rspec_example) }
-
- before do
- allow(Kernel).to receive(:warn)
- end
-
- context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do
- it 'delegates the writes to RspecFlaky::Report' do
- listener.example_passed(notification_new_flaky_rspec_example)
- listener.example_passed(notification_already_flaky_rspec_example)
-
- report1 = double
- report2 = double
-
- expect(RspecFlaky::Report).to receive(:new).with(listener.flaky_examples).and_return(report1)
- expect(report1).to receive(:write).with(RspecFlaky::Config.flaky_examples_report_path)
-
- expect(RspecFlaky::Report).to receive(:new).with(listener.__send__(:new_flaky_examples)).and_return(report2)
- expect(report2).to receive(:write).with(RspecFlaky::Config.new_flaky_examples_report_path)
-
- listener.dump_summary(nil)
- end
- end
- end
-end
diff --git a/spec/tooling/rspec_flaky/report_spec.rb b/spec/tooling/rspec_flaky/report_spec.rb
deleted file mode 100644
index e7365c1e150..00000000000
--- a/spec/tooling/rspec_flaky/report_spec.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-# frozen_string_literal: true
-
-require 'tempfile'
-
-require_relative '../../support/time_travel'
-
-require_relative '../../../tooling/rspec_flaky/report'
-
-RSpec.describe RspecFlaky::Report, :aggregate_failures, :freeze_time do
- let(:thirty_one_days) { 3600 * 24 * 31 }
- let(:collection_hash) do
- {
- a: { example_id: 'spec/foo/bar_spec.rb:2' },
- b: { example_id: 'spec/foo/baz_spec.rb:3', first_flaky_at: (Time.now - thirty_one_days).to_s, last_flaky_at: (Time.now - thirty_one_days).to_s }
- }
- end
-
- let(:suite_flaky_example_report) do
- {
- '6e869794f4cfd2badd93eb68719371d1': {
- example_id: 'spec/foo/bar_spec.rb:2',
- file: 'spec/foo/bar_spec.rb',
- line: 2,
- description: 'hello world',
- first_flaky_at: 1234,
- last_flaky_at: 4321,
- last_attempts_count: 3,
- flaky_reports: 1,
- feature_category: 'feature_category',
- last_flaky_job: nil
- }
- }
- end
-
- let(:flaky_examples) { RspecFlaky::FlakyExamplesCollection.new(collection_hash) }
- let(:report) { described_class.new(flaky_examples) }
-
- before do
- allow(Kernel).to receive(:warn)
- end
-
- describe '.load' do
- let!(:report_file) do
- Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
- f.write(JSON.pretty_generate(suite_flaky_example_report)) # rubocop:disable Gitlab/Json
- f.rewind
- end
- end
-
- after do
- report_file.close
- report_file.unlink
- end
-
- it 'loads the report file' do
- expect(described_class.load(report_file.path).flaky_examples.to_h).to eq(suite_flaky_example_report)
- end
- end
-
- describe '.load_json' do
- let(:report_json) do
- JSON.pretty_generate(suite_flaky_example_report) # rubocop:disable Gitlab/Json
- end
-
- it 'loads the report file' do
- expect(described_class.load_json(report_json).flaky_examples.to_h).to eq(suite_flaky_example_report)
- end
- end
-
- describe '#initialize' do
- it 'accepts a RspecFlaky::FlakyExamplesCollection' do
- expect { report }.not_to raise_error
- end
-
- it 'does not accept anything else' do
- expect { described_class.new([1, 2, 3]) }.to raise_error(ArgumentError, "`flaky_examples` must be a RspecFlaky::FlakyExamplesCollection, Array given!")
- end
- end
-
- it 'delegates to #flaky_examples using SimpleDelegator' do
- expect(report.__getobj__).to eq(flaky_examples)
- end
-
- describe '#write' do
- let(:report_file_path) { File.join('tmp', 'rspec_flaky_report.json') }
-
- before do
- FileUtils.rm(report_file_path) if File.exist?(report_file_path)
- end
-
- after do
- FileUtils.rm(report_file_path) if File.exist?(report_file_path)
- end
-
- context 'when RspecFlaky::Config.generate_report? is false' do
- before do
- allow(RspecFlaky::Config).to receive(:generate_report?).and_return(false)
- end
-
- it 'does not write any report file' do
- report.write(report_file_path)
-
- expect(File.exist?(report_file_path)).to be(false)
- end
- end
-
- context 'when RspecFlaky::Config.generate_report? is true' do
- before do
- allow(RspecFlaky::Config).to receive(:generate_report?).and_return(true)
- end
-
- it 'delegates the writes to RspecFlaky::Report' do
- report.write(report_file_path)
-
- expect(File.exist?(report_file_path)).to be(true)
- expect(File.read(report_file_path))
- .to eq(JSON.pretty_generate(report.flaky_examples.to_h)) # rubocop:disable Gitlab/Json
- end
- end
- end
-
- describe '#prune_outdated' do
- it 'returns a new collection without the examples older than 30 days by default' do
- new_report = flaky_examples.to_h.dup.tap { |r| r.delete(:b) }
- new_flaky_examples = report.prune_outdated
-
- expect(new_flaky_examples).to be_a(described_class)
- expect(new_flaky_examples.to_h).to eq(new_report)
- expect(flaky_examples).to have_key(:b)
- end
-
- it 'accepts a given number of days' do
- new_flaky_examples = report.prune_outdated(days: 32)
-
- expect(new_flaky_examples.to_h).to eq(report.to_h)
- end
- end
-end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index e472ac46e66..bba7eb78f99 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe AvatarUploader do
end
end
- context 'accept whitelist file content type' do
+ context 'accept allowlist file content type' do
# We need to feed through a valid path, but we force the parsed mime type
# in a stub below so we can set any path.
let_it_be(:path) { File.join('spec', 'fixtures', 'video_sample.mp4') }
@@ -61,13 +61,13 @@ RSpec.describe AvatarUploader do
end
end
- context 'upload non-whitelisted file content type' do
+ context 'upload denylisted file content type' do
let_it_be(:path) { File.join('spec', 'fixtures', 'sanitized.svg') }
it_behaves_like 'denied carrierwave upload'
end
- context 'upload misnamed non-whitelisted file content type' do
+ context 'upload misnamed denylisted file content type' do
let_it_be(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
it_behaves_like 'denied carrierwave upload'
diff --git a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
index f3dd77d67a0..3991058b32d 100644
--- a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
+++ b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe DesignManagement::DesignV432x230Uploader do
)
end
- context 'accept whitelist file content type' do
+ context 'accept allowlisted file content type' do
# We need to feed through a valid path, but we force the parsed mime type
# in a stub below so we can set any path.
let_it_be(:path) { File.join('spec', 'fixtures', 'dk.png') }
@@ -72,13 +72,13 @@ RSpec.describe DesignManagement::DesignV432x230Uploader do
end
end
- context 'upload non-whitelisted file content type' do
+ context 'upload denylisted file content type' do
let_it_be(:path) { File.join('spec', 'fixtures', 'logo_sample.svg') }
it_behaves_like 'denied carrierwave upload'
end
- context 'upload misnamed non-whitelisted file content type' do
+ context 'upload misnamed denylisted file content type' do
let_it_be(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
it_behaves_like 'denied carrierwave upload'
diff --git a/spec/uploaders/favicon_uploader_spec.rb b/spec/uploaders/favicon_uploader_spec.rb
index 7f452075293..ab14397c27d 100644
--- a/spec/uploaders/favicon_uploader_spec.rb
+++ b/spec/uploaders/favicon_uploader_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe FaviconUploader do
let_it_be(:model) { build_stubbed(:user) }
let_it_be(:uploader) { described_class.new(model, :favicon) }
- context 'accept whitelist file content type' do
+ context 'accept allowlist file content type' do
include_context 'ignore extension allowlist check'
# We need to feed through a valid path, but we force the parsed mime type
@@ -22,7 +22,7 @@ RSpec.describe FaviconUploader do
end
end
- context 'upload non-whitelisted file content type' do
+ context 'upload denylisted file content type' do
include_context 'ignore extension allowlist check'
let_it_be(:path) { File.join('spec', 'fixtures', 'sanitized.svg') }
@@ -30,7 +30,7 @@ RSpec.describe FaviconUploader do
it_behaves_like 'denied carrierwave upload'
end
- context 'upload misnamed non-whitelisted file content type' do
+ context 'upload misnamed denylisted file content type' do
include_context 'ignore extension allowlist check'
let_it_be(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
diff --git a/spec/validators/cron_validator_spec.rb b/spec/validators/cron_validator_spec.rb
index bd7fe242957..52a57f6e160 100644
--- a/spec/validators/cron_validator_spec.rb
+++ b/spec/validators/cron_validator_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe CronValidator do
expect(subject.valid?).to be_falsy
end
- context 'cron field is not whitelisted' do
+ context 'cron field is not allowlisted' do
subject do
Class.new do
include ActiveModel::Model
@@ -43,7 +43,7 @@ RSpec.describe CronValidator do
it 'raises an error' do
subject.cron_partytime = '0 23 * * 5'
- expect { subject.valid? }.to raise_error(StandardError, "Non-whitelisted attribute")
+ expect { subject.valid? }.to raise_error(StandardError, "Non-allowlisted attribute")
end
end
end
diff --git a/spec/validators/html_safety_validator_spec.rb b/spec/validators/html_safety_validator_spec.rb
index 4d9425235e3..b33d1dcab6c 100644
--- a/spec/validators/html_safety_validator_spec.rb
+++ b/spec/validators/html_safety_validator_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe HtmlSafetyValidator do
it 'adds an error when a script is included in the name' do
validate('My group <script>evil_script</script>')
- expect(group.errors[:name]).to eq([HtmlSafetyValidator.error_message])
+ expect(group.errors[:name]).to eq([described_class.error_message])
end
it 'does not add an error when an ampersand is included in the name' do
diff --git a/spec/views/admin/application_settings/_slack.html.haml_spec.rb b/spec/views/admin/application_settings/_slack.html.haml_spec.rb
new file mode 100644
index 00000000000..6f89d2b7de4
--- /dev/null
+++ b/spec/views/admin/application_settings/_slack.html.haml_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'admin/application_settings/_slack.html.haml', feature_category: :integrations do
+ let(:app_settings) { build(:application_setting) }
+
+ before do
+ assign(:application_setting, app_settings)
+ end
+
+ it 'renders the form correctly', :aggregate_failures do
+ render
+
+ expect(rendered).to have_field('Client ID', type: 'text')
+ expect(rendered).to have_field('Client secret', type: 'text')
+ expect(rendered).to have_field('Signing secret', type: 'text')
+ expect(rendered).to have_field('Verification token', type: 'text')
+ expect(rendered).to have_link(
+ 'Create Slack app',
+ href: slack_app_manifest_share_admin_application_settings_path
+ )
+ expect(rendered).to have_link(
+ 'Download latest manifest file',
+ href: slack_app_manifest_download_admin_application_settings_path
+ )
+ end
+
+ context 'when GitLab.com', :saas do
+ it 'renders the form correctly', :aggregate_failures do
+ render
+
+ expect(rendered).to have_field('Client ID', type: 'text')
+ expect(rendered).to have_field('Client secret', type: 'text')
+ expect(rendered).to have_field('Signing secret', type: 'text')
+ expect(rendered).to have_field('Verification token', type: 'text')
+
+ expect(rendered).not_to have_link('Create Slack app')
+ expect(rendered).not_to have_link('Download latest manifest file')
+ end
+ end
+end
diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb
index 861f3fffa83..ee518041fbd 100644
--- a/spec/views/admin/application_settings/general.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/general.html.haml_spec.rb
@@ -96,7 +96,10 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
it 'expects display token and reset token to be available' do
expect(rendered).to have_content(app_settings.error_tracking_access_token)
- expect(rendered).to have_button('Reset error tracking access token')
+ expect(rendered).to have_link(
+ 'Reset error tracking access token',
+ href: reset_error_tracking_access_token_admin_application_settings_url
+ )
end
end
diff --git a/spec/views/explore/projects/topic.html.haml_spec.rb b/spec/views/explore/projects/topic.html.haml_spec.rb
new file mode 100644
index 00000000000..1d2085b3be6
--- /dev/null
+++ b/spec/views/explore/projects/topic.html.haml_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'explore/projects/topic.html.haml', feature_category: :groups_and_projects do
+ let(:topic) { build_stubbed(:topic, name: 'test-topic', title: 'Test topic') }
+ let(:project) { build_stubbed(:project, :public, topic_list: topic.name) }
+
+ before do
+ assign(:topic, topic)
+ assign(:projects, [project])
+
+ controller.params[:controller] = 'explore/projects'
+ controller.params[:action] = 'topic'
+
+ allow(view).to receive(:current_user).and_return(nil)
+
+ render
+ end
+
+ it 'renders atom feed button with matching path' do
+ expect(rendered).to have_link(href: topic_explore_projects_path(topic.name, format: 'atom'))
+ end
+end
diff --git a/spec/views/groups/packages/index.html.haml_spec.rb b/spec/views/groups/packages/index.html.haml_spec.rb
index 26f6268a224..3c6305d1ed9 100644
--- a/spec/views/groups/packages/index.html.haml_spec.rb
+++ b/spec/views/groups/packages/index.html.haml_spec.rb
@@ -36,4 +36,22 @@ RSpec.describe 'groups/packages/index.html.haml', feature_category: :package_reg
)
end
end
+
+ describe 'can_delete_packages' do
+ it 'without permission sets false' do
+ allow(view).to receive(:can_delete_group_packages?).and_return(false)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="false"]')
+ end
+
+ it 'with permission sets true' do
+ allow(view).to receive(:can_delete_group_packages?).and_return(true)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="true"]')
+ end
+ end
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index a44c69748e5..504a9492d7a 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -44,13 +44,13 @@ RSpec.describe 'layouts/_head' do
it 'adds a link dns-prefetch tag' do
render
- expect(rendered).to match(%Q(<link href="#{asset_host}" rel="dns-prefetch">))
+ expect(rendered).to match(%(<link href="#{asset_host}" rel="dns-prefetch">))
end
it 'adds a link preconnect tag' do
render
- expect(rendered).to match(%Q(<link crossorigin="" href="#{asset_host}" rel="preconnect">))
+ expect(rendered).to match(%(<link crossorigin="" href="#{asset_host}" rel="preconnect">))
end
end
@@ -59,7 +59,7 @@ RSpec.describe 'layouts/_head' do
render
- expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />')
+ expect(rendered).to match('<link rel="stylesheet" href="/stylesheets/highlight/themes/solarised-light.css" media="all" />')
end
context 'when an asset_host is set and snowplow url is set', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346542' do
@@ -82,7 +82,7 @@ RSpec.describe 'layouts/_head' do
it 'adds a link preconnect tag' do
render
- expect(rendered).to match(%Q(<link crossorigin="" href="#{snowplow_collector_hostname}" rel="preconnect">))
+ expect(rendered).to match(%(<link crossorigin="" href="#{snowplow_collector_hostname}" rel="preconnect">))
end
end
diff --git a/spec/views/layouts/application.html.haml_spec.rb b/spec/views/layouts/application.html.haml_spec.rb
index d4d40a9ade9..a3613329984 100644
--- a/spec/views/layouts/application.html.haml_spec.rb
+++ b/spec/views/layouts/application.html.haml_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'layouts/application' do
end
it_behaves_like 'a layout which reflects the application theme setting'
+ it_behaves_like 'a layout which reflects the preferred language'
describe "visual review toolbar" do
context "ENV['REVIEW_APPS_ENABLED'] is set to true" do
diff --git a/spec/views/layouts/devise.html.haml_spec.rb b/spec/views/layouts/devise.html.haml_spec.rb
index a9215730370..9c31f4984fa 100644
--- a/spec/views/layouts/devise.html.haml_spec.rb
+++ b/spec/views/layouts/devise.html.haml_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'layouts/devise', feature_category: :user_management do
it_behaves_like 'a layout which reflects the application theme setting'
+ it_behaves_like 'a layout which reflects the preferred language'
describe 'logo' do
it 'renders GitLab logo' do
diff --git a/spec/views/layouts/devise_empty.html.haml_spec.rb b/spec/views/layouts/devise_empty.html.haml_spec.rb
index 06d742e74dd..8c4c421922b 100644
--- a/spec/views/layouts/devise_empty.html.haml_spec.rb
+++ b/spec/views/layouts/devise_empty.html.haml_spec.rb
@@ -4,4 +4,5 @@ require 'spec_helper'
RSpec.describe 'layouts/devise_empty' do
it_behaves_like 'a layout which reflects the application theme setting'
+ it_behaves_like 'a layout which reflects the preferred language'
end
diff --git a/spec/views/layouts/fullscreen.html.haml_spec.rb b/spec/views/layouts/fullscreen.html.haml_spec.rb
index 7b345fea2ad..2309e885b75 100644
--- a/spec/views/layouts/fullscreen.html.haml_spec.rb
+++ b/spec/views/layouts/fullscreen.html.haml_spec.rb
@@ -35,6 +35,7 @@ RSpec.describe 'layouts/fullscreen' do
end
it_behaves_like 'a layout which reflects the application theme setting'
+ it_behaves_like 'a layout which reflects the preferred language'
describe 'sidebar' do
context 'when nav is set' do
diff --git a/spec/views/layouts/signup_onboarding.html.haml_spec.rb b/spec/views/layouts/signup_onboarding.html.haml_spec.rb
index 8748c673616..24fba191cab 100644
--- a/spec/views/layouts/signup_onboarding.html.haml_spec.rb
+++ b/spec/views/layouts/signup_onboarding.html.haml_spec.rb
@@ -4,4 +4,5 @@ require 'spec_helper'
RSpec.describe 'layouts/signup_onboarding' do
it_behaves_like 'a layout which reflects the application theme setting'
+ it_behaves_like 'a layout which reflects the preferred language'
end
diff --git a/spec/views/layouts/simple_registration.html.haml_spec.rb b/spec/views/layouts/simple_registration.html.haml_spec.rb
deleted file mode 100644
index 98553a12ad8..00000000000
--- a/spec/views/layouts/simple_registration.html.haml_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'layouts/simple_registration' do
- it_behaves_like 'a layout which reflects the application theme setting'
-end
diff --git a/spec/views/layouts/terms.html.haml_spec.rb b/spec/views/layouts/terms.html.haml_spec.rb
index 520882449c5..7bf97debbf9 100644
--- a/spec/views/layouts/terms.html.haml_spec.rb
+++ b/spec/views/layouts/terms.html.haml_spec.rb
@@ -10,4 +10,5 @@ RSpec.describe 'layouts/terms' do
end
it_behaves_like 'a layout which reflects the application theme setting'
+ it_behaves_like 'a layout which reflects the preferred language'
end
diff --git a/spec/views/notify/approved_merge_request_email.html.haml_spec.rb b/spec/views/notify/approved_merge_request_email.html.haml_spec.rb
index 7d19e628eb8..64a2e4f8573 100644
--- a/spec/views/notify/approved_merge_request_email.html.haml_spec.rb
+++ b/spec/views/notify/approved_merge_request_email.html.haml_spec.rb
@@ -23,4 +23,6 @@ RSpec.describe 'notify/approved_merge_request_email.html.haml' do
expect(rendered).to have_content("was approved by")
expect(rendered).to have_content(user.name.to_s)
end
+
+ it_behaves_like 'a layout which reflects the preferred language'
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 c3d320a837b..2165cb6af85 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
@@ -8,6 +8,10 @@ RSpec.describe 'notify/import_issues_csv_email.html.haml' do
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 } }
+ let(:milestone_error_results) do
+ { success: 0,
+ preprocess_errors: { milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 15.11] } } } }
+ end
before do
assign(:user, user)
@@ -58,4 +62,29 @@ a delimited text file that uses a comma to separate values.")
Please make sure it has the correct format: a delimited text file that uses a comma to separate values.")
end
end
+
+ context 'when preprocess errors reported while importing' do
+ before do
+ assign(:results, milestone_error_results)
+ end
+
+ it 'renders with project name error' do
+ render
+
+ expect(rendered).to have_content("Could not find the following milestone values in \
+#{project.full_name}: 15.10, 15.11")
+ end
+
+ context 'with a project in a group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ it 'renders with group clause error' do
+ render
+
+ expect(rendered).to have_content("Could not find the following milestone values in #{project.full_name} \
+or its parent groups: 15.10, 15.11")
+ end
+ end
+ end
end
diff --git a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
index defd8190eda..1623c375754 100644
--- a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
@@ -2,9 +2,22 @@
require 'spec_helper'
-RSpec.describe 'notify/pipeline_failed_email.html.haml' do
- it_behaves_like 'pipeline status changes email' do
+RSpec.describe 'notify/pipeline_failed_email.html.haml', feature_category: :continuous_integration do
+ context 'when pipeline has a name attribute' do
+ before do
+ build_stubbed(:ci_pipeline_metadata, pipeline: pipeline, name: "My Pipeline")
+ end
+
+ let(:title) { "Pipeline #{pipeline.name} has failed!" }
+ let(:status) { :failed }
+
+ it_behaves_like 'pipeline status changes email'
+ end
+
+ context 'when pipeline does not have a name attribute' do
let(:title) { "Pipeline ##{pipeline.id} has failed!" }
let(:status) { :failed }
+
+ it_behaves_like 'pipeline status changes email'
end
end
diff --git a/spec/views/notify/pipeline_failed_email.text.erb_spec.rb b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
index 9bd5722954f..b22a670d895 100644
--- a/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
+++ b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
@@ -1,55 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'notify/pipeline_failed_email.text.erb' do
- include Devise::Test::ControllerHelpers
-
- let(:user) { create(:user, developer_projects: [project]) }
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, :simple, source_project: project) }
-
- let(:pipeline) do
- create(
- :ci_pipeline,
- :failed,
- project: project,
- user: user,
- ref: project.default_branch,
- sha: project.commit.sha
- )
- end
-
- before do
- assign(:project, project)
- assign(:pipeline, pipeline)
- assign(:merge_request, merge_request)
- end
-
- shared_examples_for 'renders the pipeline failed email correctly' do
- it 'renders the email correctly' do
- render
-
- expect(rendered).to have_content("Pipeline ##{pipeline.id} has failed!")
- expect(rendered).to have_content(pipeline.project.name)
- expect(rendered).to have_content(pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' '))
- expect(rendered).to have_content(pipeline.commit.author_name)
- expect(rendered).to have_content("##{pipeline.id}")
- expect(rendered).to have_content(pipeline.user.name)
- expect(rendered).to have_content(build.id)
+RSpec.describe 'notify/pipeline_failed_email.text.erb', feature_category: :continuous_integration do
+ context 'when pipeline has a name attribute' do
+ before do
+ build_stubbed(:ci_pipeline_metadata, pipeline: pipeline, name: "My Pipeline")
end
- it_behaves_like 'correct pipeline information for pipelines for merge requests'
- end
-
- context 'when the pipeline contains a failed job' do
- let!(:build) { create(:ci_build, :failed, pipeline: pipeline, project: pipeline.project) }
+ let(:title) { "Pipeline #{pipeline.name} has failed!" }
+ let(:status) { :failed }
- it_behaves_like 'renders the pipeline failed email correctly'
+ it_behaves_like 'pipeline status changes email'
end
- context 'when the latest failed job is a bridge job' do
- let!(:build) { create(:ci_bridge, status: :failed, pipeline: pipeline, project: pipeline.project) }
+ context 'when pipeline does not have a name attribute' do
+ let(:title) { "Pipeline ##{pipeline.id} has failed!" }
+ let(:status) { :failed }
- it_behaves_like 'renders the pipeline failed email correctly'
+ it_behaves_like 'pipeline status changes email'
end
end
diff --git a/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb b/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb
index bdfc8fb5f6b..4c26de4650e 100644
--- a/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb
@@ -2,9 +2,22 @@
require 'spec_helper'
-RSpec.describe 'notify/pipeline_fixed_email.html.haml' do
- it_behaves_like 'pipeline status changes email' do
+RSpec.describe 'notify/pipeline_fixed_email.html.haml', feature_category: :continuous_integration do
+ context 'when pipeline has a name attribute' do
+ before do
+ build_stubbed(:ci_pipeline_metadata, pipeline: pipeline, name: "My Pipeline")
+ end
+
+ let(:title) { "Pipeline has been fixed and #{pipeline.name} has passed!" }
+ let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
+ end
+
+ context 'when pipeline does not have a name attribute' do
let(:title) { "Pipeline has been fixed and ##{pipeline.id} has passed!" }
let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
end
end
diff --git a/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb b/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb
index d0bc110f95c..dae2991b775 100644
--- a/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb
+++ b/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb
@@ -2,9 +2,22 @@
require 'spec_helper'
-RSpec.describe 'notify/pipeline_fixed_email.text.erb' do
- it_behaves_like 'pipeline status changes email' do
+RSpec.describe 'notify/pipeline_fixed_email.text.erb', feature_category: :continuous_integration do
+ context 'when pipeline has a name attribute' do
+ before do
+ build_stubbed(:ci_pipeline_metadata, pipeline: pipeline, name: "My Pipeline")
+ end
+
+ let(:title) { "Pipeline has been fixed and #{pipeline.name} has passed!" }
+ let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
+ end
+
+ context 'when pipeline does not have a name attribute' do
let(:title) { "Pipeline has been fixed and ##{pipeline.id} has passed!" }
let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
end
end
diff --git a/spec/views/notify/pipeline_success_email.html.haml_spec.rb b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
index ce03f672700..d6958c5f491 100644
--- a/spec/views/notify/pipeline_success_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
@@ -2,9 +2,22 @@
require 'spec_helper'
-RSpec.describe 'notify/pipeline_success_email.html.haml' do
- it_behaves_like 'pipeline status changes email' do
+RSpec.describe 'notify/pipeline_success_email.html.haml', feature_category: :continuous_integration do
+ context 'when pipeline has a name attribute' do
+ before do
+ build_stubbed(:ci_pipeline_metadata, pipeline: pipeline, name: "My Pipeline")
+ end
+
+ let(:title) { "Pipeline #{pipeline.name} has passed!" }
+ let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
+ end
+
+ context 'when pipeline does not have a name attribute' do
let(:title) { "Pipeline ##{pipeline.id} has passed!" }
let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
end
end
diff --git a/spec/views/notify/pipeline_success_email.text.erb_spec.rb b/spec/views/notify/pipeline_success_email.text.erb_spec.rb
index 02334a48fa3..ac1ff68b81d 100644
--- a/spec/views/notify/pipeline_success_email.text.erb_spec.rb
+++ b/spec/views/notify/pipeline_success_email.text.erb_spec.rb
@@ -2,9 +2,22 @@
require 'spec_helper'
-RSpec.describe 'notify/pipeline_success_email.text.erb' do
- it_behaves_like 'pipeline status changes email' do
+RSpec.describe 'notify/pipeline_success_email.text.erb', feature_category: :continuous_integration do
+ context 'when pipeline has a name attribute' do
+ before do
+ build_stubbed(:ci_pipeline_metadata, pipeline: pipeline, name: "My Pipeline")
+ end
+
+ let(:title) { "Pipeline #{pipeline.name} has passed!" }
+ let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
+ end
+
+ context 'when pipeline does not have a name attribute' do
let(:title) { "Pipeline ##{pipeline.id} has passed!" }
let(:status) { :success }
+
+ it_behaves_like 'pipeline status changes email'
end
end
diff --git a/spec/views/profiles/keys/_key_details.html.haml_spec.rb b/spec/views/profiles/keys/_key_details.html.haml_spec.rb
index c223d6702c5..c0381594feb 100644
--- a/spec/views/profiles/keys/_key_details.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key_details.html.haml_spec.rb
@@ -29,4 +29,19 @@ RSpec.describe 'profiles/keys/_key_details.html.haml' do
end
end
end
+
+ describe 'displays key attributes' do
+ let(:key) { create(:key, :expired, last_used_at: Date.today, user: user) }
+
+ it 'renders key attributes' do
+ render
+
+ expect(rendered).to have_text(key.title)
+ expect(rendered).to have_text(key.created_at.to_fs(:medium))
+ expect(rendered).to have_text(key.expires_at.to_fs(:medium))
+ expect(rendered).to have_text(key.last_used_at.to_fs(:medium))
+ expect(rendered).to have_text(key.fingerprint)
+ expect(rendered).to have_text(key.fingerprint_sha256)
+ end
+ end
end
diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb
index ea0a9ebb02c..e88b1bf4053 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/profiles/show.html.haml_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'profiles/show' do
)
expect(rendered).to have_field(
'user[status][clear_status_after]',
- with: user_status.clear_status_at.to_s(:iso8601),
+ with: user_status.clear_status_at.to_fs(:iso8601),
type: :hidden
)
end
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
index 6d2237e773e..4cfff00d390 100644
--- a/spec/views/projects/commit/show.html.haml_spec.rb
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -71,14 +71,12 @@ RSpec.describe 'projects/commit/show.html.haml', feature_category: :source_code_
let(:title) { badge_attributes['data-title'].value }
let(:content) { badge_attributes['data-content'].value }
- before do
- render
- end
-
context 'with GPG' do
let(:commit) { project.commit(GpgHelpers::SIGNED_COMMIT_SHA) }
it 'renders unverified badge' do
+ render
+
expect(title).to include('This commit was signed with an unverified signature.')
expect(content).to include(commit.signature.gpg_key_primary_keyid)
end
@@ -88,15 +86,34 @@ RSpec.describe 'projects/commit/show.html.haml', feature_category: :source_code_
let(:commit) { project.commit('7b5160f9bb23a3d58a0accdbe89da13b96b1ece9') }
it 'renders unverified badge' do
+ render
+
expect(title).to include('This commit was signed with an unverified signature.')
expect(content).to match(/SSH key fingerprint:[\s\S].+#{commit.signature.key_fingerprint_sha256}/)
end
+
+ context 'when the commit has been signed by GitLab' do
+ it 'renders verified badge' do
+ allow_next_instance_of(Gitlab::Ssh::Commit) do |instance|
+ allow(instance).to receive(:signer).and_return(:SIGNER_SYSTEM)
+ end
+
+ render
+
+ expect(content).to match(/SSH key fingerprint:[\s\S].+#{commit.signature.key_fingerprint_sha256}/)
+ expect(title).to include(
+ 'This commit was created in the GitLab UI, and signed with a GitLab-verified signature.'
+ )
+ end
+ end
end
context 'with X.509' do
let(:commit) { project.commit('189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
it 'renders unverified badge' do
+ render
+
expect(title).to include('This commit was signed with an <strong>unverified</strong> signature.')
expect(content).to include(commit.signature.x509_certificate.subject_key_identifier.tr(":", " "))
end
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 77336aa7d86..8c1a8cf21d0 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -105,26 +105,10 @@ RSpec.describe 'projects/edit' do
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
- it 'does show a callout' do
- render
- expect(rendered).to have_content('GitLab Pages has moved')
- end
+ expect(rendered).to have_content(_('GitLab Pages has moved'))
end
end
end
diff --git a/spec/views/projects/packages/index.html.haml_spec.rb b/spec/views/projects/packages/index.html.haml_spec.rb
index 2557ceb70b3..e59db289ad4 100644
--- a/spec/views/projects/packages/index.html.haml_spec.rb
+++ b/spec/views/projects/packages/index.html.haml_spec.rb
@@ -36,4 +36,22 @@ RSpec.describe 'projects/packages/packages/index.html.haml', feature_category: :
)
end
end
+
+ describe 'can_delete_packages' do
+ it 'without permission sets empty settings path' do
+ allow(view).to receive(:can_delete_packages?).and_return(false)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="false"]')
+ end
+
+ it 'with permission sets project settings path' do
+ allow(view).to receive(:can_delete_packages?).and_return(true)
+
+ render
+
+ expect(rendered).to have_selector('[data-can-delete-packages="true"]')
+ end
+ end
end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index 3c15d5846e9..81a11874886 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_compositio
let(:presented_pipeline) { pipeline.present(current_user: user) }
before do
- stub_feature_flags(pipeline_details_header_vue: false)
assign(:project, project)
assign(:pipeline, presented_pipeline)
allow(view).to receive(:current_user) { user }
diff --git a/spec/views/projects/runners/_project_runners.html.haml_spec.rb b/spec/views/projects/runners/_project_runners.html.haml_spec.rb
index d96b77b368c..7dd5e829686 100644
--- a/spec/views/projects/runners/_project_runners.html.haml_spec.rb
+++ b/spec/views/projects/runners/_project_runners.html.haml_spec.rb
@@ -15,66 +15,27 @@ RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category:
allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
end
- context 'when create_runner_workflow_for_namespace is disabled' do
+ context 'when user can create project runner' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: false)
+ allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
end
- context 'when project runner registration is allowed' do
- before do
- stub_application_setting(valid_runner_registrars: ['project'])
- allow(view).to receive(:can?).with(user, :register_project_runners, project).and_return(true)
- end
+ it 'renders the New project runner button' do
+ render 'projects/runners/project_runners', project: project
- it 'enables the Remove project button for a project' do
- render 'projects/runners/project_runners', project: project
-
- expect(rendered).to have_selector '#js-install-runner'
- expect(rendered).not_to have_content 'Please contact an admin to register runners.'
- end
- end
-
- context 'when project runner registration is not allowed' do
- before do
- stub_application_setting(valid_runner_registrars: ['group'])
- end
-
- it 'does not enable the Remove project button for a project' do
- render 'projects/runners/project_runners', project: project
-
- expect(rendered).to have_content 'Please contact an admin to register runners.'
- expect(rendered).not_to have_selector '#js-install-runner'
- end
+ expect(rendered).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
end
end
- context 'when create_runner_workflow_for_namespace is enabled' do
+ context 'when user cannot create project runner' do
before do
- stub_feature_flags(create_runner_workflow_for_namespace: project.namespace)
+ allow(view).to receive(:can?).with(user, :create_runner, project).and_return(false)
end
- context 'when user can create project runner' do
- before do
- allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
- end
-
- it 'renders the New project runner button' do
- render 'projects/runners/project_runners', project: project
-
- expect(rendered).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
- end
- end
-
- context 'when user cannot create project runner' do
- before do
- allow(view).to receive(:can?).with(user, :create_runner, project).and_return(false)
- end
-
- it 'does not render the New project runner button' do
- render 'projects/runners/project_runners', project: project
+ it 'does not render the New project runner button' do
+ render 'projects/runners/project_runners', project: project
- expect(rendered).not_to have_link(s_('Runners|New project runner'))
- end
+ expect(rendered).not_to have_link(s_('Runners|New project runner'))
end
end
end
diff --git a/spec/views/registrations/welcome/show.html.haml_spec.rb b/spec/views/registrations/welcome/show.html.haml_spec.rb
index e229df555b1..866f4f62493 100644
--- a/spec/views/registrations/welcome/show.html.haml_spec.rb
+++ b/spec/views/registrations/welcome/show.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'registrations/welcome/show' do
+RSpec.describe 'registrations/welcome/show', feature_category: :onboarding do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb
index 832cc5b7cf3..ebeb4226434 100644
--- a/spec/views/search/_results.html.haml_spec.rb
+++ b/spec/views/search/_results.html.haml_spec.rb
@@ -68,7 +68,8 @@ RSpec.describe 'search/_results', feature_category: :global_search do
context 'rendering all types of search results' do
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
- let_it_be(:issue) { create(:issue, project: project, title: 'testing') }
+ let_it_be(:label) { create(:label, project: project, title: 'test label') }
+ let_it_be(:issue) { create(:issue, project: project, title: 'testing', labels: [label]) }
let_it_be(:merge_request) { create(:merge_request, title: 'testing', source_project: project, target_project: project) }
let_it_be(:milestone) { create(:milestone, title: 'testing', project: project) }
let_it_be(:note) { create(:discussion_note_on_issue, project: project, note: 'testing') }
diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
deleted file mode 100644
index ccf1e08b7e7..00000000000
--- a/spec/views/shared/notes/_form.html.haml_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'shared/notes/_form' do
- include Devise::Test::ControllerHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- before do
- project.add_maintainer(user)
- assign(:project, project)
- assign(:note, note)
-
- allow(view).to receive(:current_user).and_return(user)
-
- render
- end
-
- %w[issue merge_request commit].each do |noteable|
- context "with a note on #{noteable}" do
- let(:note) { build(:"note_on_#{noteable}", project: project) }
-
- it 'says that markdown and quick actions are supported' do
- expect(rendered).to have_content('Supports Markdown. For quick actions, type /.')
- end
- end
- end
-end
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index dada4ef63b3..8238721df01 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
let(:job_args) { entity.id }
it 'updates pipeline trackers to enqueued state when selected' do
- worker = BulkImports::EntityWorker.new
+ worker = described_class.new
next_tracker = worker.send(:next_pipeline_trackers_for, entity.id).first
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 2faa28ba489..0acc44c5cbf 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -112,6 +112,36 @@ RSpec.describe BulkImports::ExportRequestWorker, feature_category: :importers do
it_behaves_like 'requests relations export for api resource'
end
+
+ context 'when source supports batched migration' do
+ let_it_be(:bulk_import) { create(:bulk_import, source_version: BulkImport.min_gl_version_for_migration_in_batches) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, :project_entity, source_full_path: 'foo/bar', bulk_import: bulk_import) }
+
+ it 'requests relations export & schedules entity worker' do
+ expected_url = "/projects/#{entity.source_xid}/export_relations?batched=true"
+
+ expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ expect(client).to receive(:post).with(expected_url)
+ end
+
+ described_class.new.perform(entity.id)
+ end
+
+ context 'when bulk_imports_batched_import_export feature flag is disabled' do
+ it 'requests relation export without batched param' do
+ stub_feature_flags(bulk_imports_batched_import_export: false)
+
+ expected_url = "/projects/#{entity.source_xid}/export_relations"
+
+ expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ expect(client).to receive(:post).with(expected_url)
+ end
+
+ described_class.new.perform(entity.id)
+ end
+ end
+ end
end
describe '#sidekiq_retries_exhausted' do
diff --git a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..6fe6b420f2b
--- /dev/null
+++ b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :importers do
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+
+ let(:status_event) { :finish }
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :started, :batched, entity: entity) }
+
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'finishes pipeline and enqueues entity worker' do
+ expect(BulkImports::EntityWorker)
+ .to receive(:perform_async)
+ .with(entity.id, pipeline_tracker.stage)
+
+ subject.perform(pipeline_tracker.id)
+
+ expect(pipeline_tracker.reload.finished?).to eq(true)
+ end
+
+ context 'when import is in progress' do
+ it 're-enqueues' do
+ create(:bulk_import_batch_tracker, :started, tracker: pipeline_tracker)
+
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(described_class::REQUEUE_DELAY, pipeline_tracker.id)
+
+ subject.perform(pipeline_tracker.id)
+ end
+ end
+
+ context 'when pipeline tracker is stale' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :started, :batched, :stale, entity: entity) }
+
+ it 'fails pipeline tracker and its batches' do
+ create(:bulk_import_batch_tracker, :finished, tracker: pipeline_tracker)
+
+ subject.perform(pipeline_tracker.id)
+
+ expect(pipeline_tracker.reload.failed?).to eq(true)
+ expect(pipeline_tracker.batches.first.reload.failed?).to eq(true)
+ end
+ end
+
+ context 'when pipeline is not batched' do
+ let(:pipeline_tracker) { create(:bulk_import_tracker, :started, entity: entity) }
+
+ it 'returns' do
+ expect_next_instance_of(BulkImports::Tracker) do |instance|
+ expect(instance).not_to receive(:finish!)
+ end
+
+ subject.perform(pipeline_tracker.id)
+ end
+ end
+
+ context 'when pipeline is not started' do
+ let(:status_event) { :start }
+
+ it 'returns' do
+ expect_next_instance_of(BulkImports::Tracker) do |instance|
+ expect(instance).not_to receive(:finish!)
+ end
+
+ described_class.new.perform(pipeline_tracker.id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
new file mode 100644
index 00000000000..c10e1b486ab
--- /dev/null
+++ b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+
+ let(:pipeline_class) do
+ Class.new do
+ def initialize(_); end
+
+ def run; end
+
+ def self.file_extraction_pipeline?
+ false
+ end
+ end
+ end
+
+ let(:tracker) do
+ create(
+ :bulk_import_tracker,
+ entity: entity,
+ pipeline_name: 'FakePipeline',
+ status_event: 'enqueue'
+ )
+ end
+
+ let(:batch) { create(:bulk_import_batch_tracker, :created, tracker: tracker) }
+
+ subject(:worker) { described_class.new }
+
+ before do
+ stub_const('FakePipeline', pipeline_class)
+
+ allow(subject).to receive(:jid).and_return('jid')
+ allow(entity).to receive(:pipeline_exists?).with('FakePipeline').and_return(true)
+ allow_next_instance_of(BulkImports::Groups::Stage) do |instance|
+ allow(instance)
+ .to receive(:pipelines)
+ .and_return([{ stage: 0, pipeline: pipeline_class }])
+ end
+ end
+
+ describe '#perform' do
+ it 'runs the given pipeline batch successfully' do
+ expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
+
+ subject.perform(batch.id)
+
+ expect(batch.reload).to be_finished
+ end
+
+ context 'when tracker is failed' do
+ let(:tracker) { create(:bulk_import_tracker, :failed) }
+
+ it 'skips the batch' do
+ subject.perform(batch.id)
+
+ expect(batch.reload).to be_skipped
+ end
+ end
+
+ context 'when tracker is finished' do
+ let(:tracker) { create(:bulk_import_tracker, :finished) }
+
+ it 'skips the batch' do
+ subject.perform(batch.id)
+
+ expect(batch.reload).to be_skipped
+ end
+ end
+
+ context 'when exclusive lease cannot be obtained' do
+ it 'does not run the pipeline' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ expect(subject).not_to receive(:run)
+
+ subject.perform(batch.id)
+ end
+ end
+
+ context 'when pipeline raises an exception' do
+ context 'when pipeline is retryable' do
+ it 'retries the batch' do
+ allow_next_instance_of(pipeline_class) do |instance|
+ allow(instance)
+ .to receive(:run)
+ .and_raise(BulkImports::RetryPipelineError.new('Error!', 60))
+ end
+
+ expect(described_class).to receive(:perform_in).with(60, batch.id)
+
+ subject.perform(batch.id)
+
+ expect(batch.reload).to be_created
+ end
+ end
+
+ context 'when pipeline is not retryable' do
+ it 'fails the batch and creates a failure record' do
+ allow_next_instance_of(pipeline_class) do |instance|
+ allow(instance).to receive(:run).and_raise(StandardError, 'Something went wrong')
+ end
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ hash_including(
+ batch_id: batch.id,
+ tracker_id: tracker.id,
+ pipeline_class: 'FakePipeline',
+ pipeline_step: 'pipeline_batch_worker_run'
+ )
+ )
+
+ expect(BulkImports::Failure).to receive(:create).with(
+ bulk_import_entity_id: entity.id,
+ pipeline_class: 'FakePipeline',
+ pipeline_step: 'pipeline_batch_worker_run',
+ exception_class: 'StandardError',
+ exception_message: 'Something went wrong',
+ correlation_id_value: anything
+ )
+
+ expect(BulkImports::FinishBatchedPipelineWorker).to receive(:perform_async).with(tracker.id)
+
+ subject.perform(batch.id)
+
+ expect(batch.reload).to be_failed
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index e8b0714471d..320f62dc93e 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -387,8 +387,9 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
stub_const('NdjsonPipeline', file_extraction_pipeline)
allow_next_instance_of(BulkImports::Groups::Stage) do |instance|
- allow(instance).to receive(:pipelines)
- .and_return([{ stage: 0, pipeline: file_extraction_pipeline }])
+ allow(instance)
+ .to receive(:pipelines)
+ .and_return([{ stage: 0, pipeline: file_extraction_pipeline }])
end
end
@@ -397,6 +398,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:started?).and_return(false)
allow(status).to receive(:empty?).and_return(false)
allow(status).to receive(:failed?).and_return(false)
+ allow(status).to receive(:batched?).and_return(false)
end
subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
@@ -410,6 +412,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:started?).and_return(true)
allow(status).to receive(:empty?).and_return(false)
allow(status).to receive(:failed?).and_return(false)
+ allow(status).to receive(:batched?).and_return(false)
end
expect(described_class)
@@ -431,6 +434,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:started?).and_return(false)
allow(status).to receive(:empty?).and_return(true)
allow(status).to receive(:failed?).and_return(false)
+ allow(status).to receive(:batched?).and_return(false)
end
pipeline_tracker.update!(created_at: created_at)
@@ -572,5 +576,43 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(pipeline_tracker.reload.status_name).to eq(:failed)
end
end
+
+ context 'when export is batched' do
+ let(:batches_count) { 2 }
+
+ before do
+ allow_next_instance_of(BulkImports::ExportStatus) do |status|
+ allow(status).to receive(:batched?).and_return(true)
+ allow(status).to receive(:batches_count).and_return(batches_count)
+ allow(status).to receive(:started?).and_return(false)
+ allow(status).to receive(:empty?).and_return(false)
+ allow(status).to receive(:failed?).and_return(false)
+ end
+ end
+
+ it 'enqueues pipeline batches' do
+ expect(BulkImports::PipelineBatchWorker).to receive(:perform_async).twice
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ pipeline_tracker.reload
+
+ expect(pipeline_tracker.status_name).to eq(:started)
+ expect(pipeline_tracker.batched).to eq(true)
+ expect(pipeline_tracker.batches.count).to eq(batches_count)
+ end
+
+ context 'when batches count is less than 1' do
+ let(:batches_count) { 0 }
+
+ it 'marks tracker as finished' do
+ expect(subject).not_to receive(:enqueue_batches)
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.status_name).to eq(:finished)
+ end
+ end
+ end
end
end
diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb
index f91db0388a4..646af6c2a9c 100644
--- a/spec/workers/bulk_imports/relation_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb
@@ -47,28 +47,14 @@ RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers d
context 'when export is batched' do
let(:batched) { true }
- context 'when bulk_imports_batched_import_export feature flag is disabled' do
- before do
- stub_feature_flags(bulk_imports_batched_import_export: false)
- end
-
- include_examples 'export service', BulkImports::RelationExportService
+ context 'when relation is batchable' do
+ include_examples 'export service', BulkImports::BatchedRelationExportService
end
- context 'when bulk_imports_batched_import_export feature flag is enabled' do
- before do
- stub_feature_flags(bulk_imports_batched_import_export: true)
- end
-
- context 'when relation is batchable' do
- include_examples 'export service', BulkImports::BatchedRelationExportService
- end
+ context 'when relation is not batchable' do
+ let(:relation) { 'namespace_settings' }
- context 'when relation is not batchable' do
- let(:relation) { 'namespace_settings' }
-
- include_examples 'export service', BulkImports::RelationExportService
- end
+ include_examples 'export service', BulkImports::RelationExportService
end
end
diff --git a/spec/workers/ci/merge_requests/cleanup_ref_worker_spec.rb b/spec/workers/ci/merge_requests/cleanup_ref_worker_spec.rb
new file mode 100644
index 00000000000..41ee1047adf
--- /dev/null
+++ b/spec/workers/ci/merge_requests/cleanup_ref_worker_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::CleanupRefWorker, :sidekiq_inline, feature_category: :code_review_workflow do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:source_project) { create(:project, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: source_project) }
+
+ let(:worker) { described_class.new }
+ let(:only) { :all }
+
+ subject { worker.perform(merge_request.id, only) }
+
+ it 'does remove all merge request refs' do
+ expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request)
+ expect(merge_request.target_project.repository)
+ .to receive(:delete_refs)
+ .with(merge_request.ref_path, merge_request.merge_ref_path, merge_request.train_ref_path)
+
+ subject
+ end
+
+ context 'when only is :train' do
+ let(:only) { :train }
+
+ it 'does remove only car merge request train ref' do
+ expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request)
+ expect(merge_request.target_project.repository)
+ .to receive(:delete_refs)
+ .with(merge_request.train_ref_path)
+
+ subject
+ end
+ end
+
+ context 'when max retry attempts reach' do
+ let(:lease_key) { "projects/#{merge_request.target_project_id}/serialized_remove_refs" }
+ let!(:lease) { stub_exclusive_lease_taken(lease_key) }
+
+ it 'does not raise error' do
+ expect(lease).to receive(:try_obtain).exactly(described_class::LOCK_RETRY + 1).times
+ expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+end
diff --git a/spec/workers/ci/pipeline_cleanup_ref_worker_spec.rb b/spec/workers/ci/pipeline_cleanup_ref_worker_spec.rb
new file mode 100644
index 00000000000..af7b505602e
--- /dev/null
+++ b/spec/workers/ci/pipeline_cleanup_ref_worker_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineCleanupRefWorker, :sidekiq_inline, feature_category: :continuous_integration do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:pipeline) { create(:ci_pipeline, :success) }
+
+ let(:worker) { described_class.new }
+
+ subject { worker.perform(pipeline.id) }
+
+ it 'does remove persistent ref' do
+ expect_next_instance_of(Ci::PersistentRef) do |persistent_ref|
+ expect(persistent_ref).to receive(:delete).once
+ end
+
+ subject
+ end
+
+ context 'when pipeline is still running' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :running) }
+
+ it 'does not remove persistent ref' do
+ expect_next_instance_of(Ci::PersistentRef) do |persistent_ref|
+ expect(persistent_ref).not_to receive(:delete)
+ end
+
+ subject
+ end
+ end
+
+ context 'when pipeline status changes while being locked' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :success) }
+
+ it 'does not remove persistent ref' do
+ expect_next_instance_of(Ci::PersistentRef) do |persistent_ref|
+ expect(persistent_ref).not_to receive(:delete_refs)
+ end
+
+ expect(worker).to receive(:in_lock).and_wrap_original do |method, *args, **kwargs, &block|
+ pipeline.run!
+
+ method.call(*args, **kwargs, &block)
+ end
+
+ subject
+ end
+ end
+
+ context 'when max retry attempts reach' do
+ let(:lease_key) { "projects/#{pipeline.project_id}/serialized_remove_refs" }
+ let!(:lease) { stub_exclusive_lease_taken(lease_key) }
+
+ it 'does not raise error' do
+ expect(lease).to receive(:try_obtain).exactly(described_class::LOCK_RETRY + 1).times
+ expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+end
diff --git a/spec/workers/container_registry/cleanup_worker_spec.rb b/spec/workers/container_registry/cleanup_worker_spec.rb
index 955d2175085..ef54bd40ede 100644
--- a/spec/workers/container_registry/cleanup_worker_spec.rb
+++ b/spec/workers/container_registry/cleanup_worker_spec.rb
@@ -71,6 +71,7 @@ RSpec.describe ContainerRegistry::CleanupWorker, :aggregate_failures, feature_ca
let(:relation) { instance_double(ActiveRecord::Relation) }
before do
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(true)
allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
allow(Project).to receive(:pending_data_repair_analysis).and_return(relation)
end
@@ -116,23 +117,5 @@ RSpec.describe ContainerRegistry::CleanupWorker, :aggregate_failures, feature_ca
it_behaves_like 'does not enqueue record repair detail jobs'
end
-
- context 'for counts logging' do
- let_it_be(:delete_started_at) { (described_class::STALE_DELETE_THRESHOLD + 5.minutes).ago }
- let_it_be(:stale_delete_container_repository) do
- create(:container_repository, :status_delete_ongoing, delete_started_at: delete_started_at)
- end
-
- before do
- container_repository.delete_scheduled!
- end
-
- it 'logs the counts' do
- expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_scheduled_container_repositories_count, 1)
- expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_delete_container_repositories_count, 1)
-
- perform
- end
- end
end
end
diff --git a/spec/workers/container_registry/record_data_repair_detail_worker_spec.rb b/spec/workers/container_registry/record_data_repair_detail_worker_spec.rb
index 118b897b26f..d8da45cfdcb 100644
--- a/spec/workers/container_registry/record_data_repair_detail_worker_spec.rb
+++ b/spec/workers/container_registry/record_data_repair_detail_worker_spec.rb
@@ -36,8 +36,17 @@ RSpec.describe ContainerRegistry::RecordDataRepairDetailWorker, :aggregate_failu
end
context 'when on Gitlab.com', :saas do
+ before do
+ allow(::Gitlab).to receive(:com_except_jh?).and_return(true)
+ end
+
it 'obtains exclusive lease on the project' do
- expect(Project).to receive(:pending_data_repair_analysis).and_call_original
+ pending_analysis = Project.pending_data_repair_analysis
+ limited_pending_analysis = Project.pending_data_repair_analysis
+ expect(pending_analysis).to receive(:limit).and_return(limited_pending_analysis)
+ expect(limited_pending_analysis).to receive(:sample).and_call_original
+
+ expect(Project).to receive(:pending_data_repair_analysis).and_return(pending_analysis)
expect_to_obtain_exclusive_lease("container_registry_data_repair_detail_worker:#{project.id}",
timeout: described_class::LEASE_TIMEOUT)
expect_to_cancel_exclusive_lease("container_registry_data_repair_detail_worker:#{project.id}", 'uuid')
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index cf1667cb0ff..38959b6d764 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -122,11 +122,9 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'AdminEmailsWorker' => 3,
'Analytics::CodeReviewMetricsWorker' => 3,
'Analytics::DevopsAdoption::CreateSnapshotWorker' => 3,
- 'Analytics::InstanceStatistics::CounterJobWorker' => 3,
'Analytics::UsageTrends::CounterJobWorker' => 3,
'ApprovalRules::ExternalApprovalRulePayloadWorker' => 3,
'ApproveBlockedPendingApprovalUsersWorker' => 3,
- 'ArchiveTraceWorker' => 3,
'AuthorizedKeysWorker' => 3,
'AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' => 3,
'AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker' => 3,
@@ -136,7 +134,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'AutoMergeProcessWorker' => 3,
'BackgroundMigrationWorker' => 3,
'BackgroundMigration::CiDatabaseWorker' => 3,
- 'BuildFinishedWorker' => 3,
'BuildHooksWorker' => 3,
'BuildQueueWorker' => 3,
'BuildSuccessWorker' => 3,
@@ -144,6 +141,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'BulkImports::ExportRequestWorker' => 5,
'BulkImports::EntityWorker' => false,
'BulkImports::PipelineWorker' => false,
+ 'BulkImports::PipelineBatchWorker' => false,
'Chaos::CpuSpinWorker' => 3,
'Chaos::DbSpinWorker' => 3,
'Chaos::KillWorker' => false,
@@ -165,6 +163,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Ci::Llm::GenerateConfigWorker' => 3,
'Ci::PipelineArtifacts::CoverageReportWorker' => 3,
'Ci::PipelineArtifacts::CreateQualityReportWorker' => 3,
+ 'Ci::PipelineCleanupRefWorker' => 3,
'Ci::PipelineBridgeStatusWorker' => 3,
'Ci::PipelineSuccessUnlockArtifactsWorker' => 3,
'Ci::RefDeleteUnlockArtifactsWorker' => 3,
@@ -186,7 +185,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Clusters::Applications::DeactivateIntegrationWorker' => 3,
'Clusters::Applications::UninstallWorker' => 3,
'Clusters::Applications::WaitForUninstallAppWorker' => 3,
- 'Clusters::Cleanup::AppWorker' => 3,
'Clusters::Cleanup::ProjectNamespaceWorker' => 3,
'Clusters::Cleanup::ServiceAccountWorker' => 3,
'ContainerExpirationPolicies::CleanupContainerRepositoryWorker' => 0,
@@ -206,10 +204,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'DependencyProxy::CleanupBlobWorker' => 3,
'DependencyProxy::CleanupManifestWorker' => 3,
'Deployments::AutoRollbackWorker' => 3,
- 'Deployments::FinishedWorker' => 3,
- 'Deployments::ForwardDeploymentWorker' => 3,
'Deployments::LinkMergeRequestWorker' => 3,
- 'Deployments::SuccessWorker' => 3,
'Deployments::UpdateEnvironmentWorker' => 3,
'Deployments::ApprovalWorker' => 3,
'DesignManagement::CopyDesignCollectionWorker' => 3,
@@ -251,6 +246,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Geo::RepositoryVerification::Primary::SingleWorker' => false,
'Geo::RepositoryVerification::Secondary::SingleWorker' => false,
'Geo::ReverificationBatchWorker' => 0,
+ 'Geo::BulkMarkPendingBatchWorker' => 0,
'Geo::Scheduler::Primary::SchedulerWorker' => false,
'Geo::Scheduler::SchedulerWorker' => false,
'Geo::Scheduler::Secondary::SchedulerWorker' => false,
@@ -259,7 +255,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Geo::VerificationTimeoutWorker' => false,
'Geo::VerificationWorker' => 3,
'GeoRepositoryDestroyWorker' => 3,
- 'GitGarbageCollectWorker' => false,
'Gitlab::BitbucketServerImport::AdvanceStageWorker' => 3,
'Gitlab::BitbucketServerImport::Stage::FinishImportWorker' => 3,
'Gitlab::BitbucketServerImport::Stage::ImportLfsObjectsWorker' => 3,
@@ -355,8 +350,8 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'MergeRequestCleanupRefsWorker' => 3,
'MergeRequestMergeabilityCheckWorker' => 3,
'MergeRequestResetApprovalsWorker' => 3,
- 'MergeRequests::AssigneesChangeWorker' => 3,
'MergeRequests::CaptureSuggestedReviewersAcceptedWorker' => 3,
+ 'MergeRequests::CleanupRefWorker' => 3,
'MergeRequests::CreatePipelineWorker' => 3,
'MergeRequests::DeleteSourceBranchWorker' => 3,
'MergeRequests::FetchSuggestedReviewersWorker' => 3,
@@ -366,15 +361,12 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'MergeRequests::SyncCodeOwnerApprovalRulesWorker' => 3,
'MergeTrains::RefreshWorker' => 3,
'MergeWorker' => 3,
- 'Metrics::Dashboard::PruneOldAnnotationsWorker' => 3,
- 'Metrics::Dashboard::SyncDashboardsWorker' => 3,
'MigrateExternalDiffsWorker' => 3,
'Onboarding::IssueCreatedWorker' => 3,
'Onboarding::PipelineCreatedWorker' => 3,
'Onboarding::ProgressWorker' => 3,
'Onboarding::UserAddedWorker' => 3,
'Namespaces::FreeUserCap::OverLimitNotificationWorker' => false,
- 'Namespaces::RefreshRootStatisticsWorker' => 3,
'Namespaces::RootStatisticsWorker' => 3,
'Namespaces::ScheduleAggregationWorker' => 3,
'Namespaces::FreeUserCap::NotificationClearingWorker' => false,
@@ -412,9 +404,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'ProjectDestroyWorker' => 3,
'ProjectExportWorker' => false,
'ProjectImportScheduleWorker' => 1,
- 'ProjectScheduleBulkRepositoryShardMovesWorker' => 3,
'ProjectTemplateExportWorker' => false,
- 'ProjectUpdateRepositoryStorageWorker' => 3,
'Projects::DeregisterSuggestedReviewersProjectWorker' => 3,
'Projects::DisableLegacyOpenSourceLicenseForInactiveProjectsWorker' => 3,
'Projects::GitGarbageCollectWorker' => false,
@@ -447,13 +437,12 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'RequirementsManagement::ProcessRequirementsReportsWorker' => 3,
'RunPipelineScheduleWorker' => 3,
'ScanSecurityReportSecretsWorker' => 17,
+ 'Search::ElasticGroupAssociationDeletionWorker' => 3,
'Security::AutoFixWorker' => 3,
'Security::StoreScansWorker' => 3,
'Security::TrackSecureScansWorker' => 1,
'ServiceDeskEmailReceiverWorker' => 3,
'SetUserStatusBasedOnUserCapSettingWorker' => 3,
- 'SnippetScheduleBulkRepositoryShardMovesWorker' => 3,
- 'SnippetUpdateRepositoryStorageWorker' => 3,
'Snippets::ScheduleBulkRepositoryShardMovesWorker' => 3,
'Snippets::UpdateRepositoryStorageWorker' => 3,
'StageUpdateWorker' => 3,
@@ -481,7 +470,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'VulnerabilityExports::ExportWorker' => 3,
'WaitForClusterCreationWorker' => 3,
'WebHookWorker' => 4,
- 'WebHooks::DestroyWorker' => 3,
'WebHooks::LogExecutionWorker' => 3,
'Wikis::GitGarbageCollectWorker' => false,
'WorkItems::ImportWorkItemsCsvWorker' => 3,
@@ -491,6 +479,23 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
}.merge(extra_retry_exceptions)
end
+ it 'defines `retry_exceptions` only for existing workers', if: Gitlab.ee? do
+ removed_workers = retry_exceptions.keys - retry_exception_workers.map { |worker| worker.klass.to_s }
+ message = -> do
+ list = removed_workers.map { |name| "- #{name}" }
+
+ <<~MESSAGE
+ The following workers no longer exist but are defined in `retry_exceptions`:
+
+ #{list.join("\n")}
+
+ Make sure to remove them from `retry_exceptions` because their definition is unnecessary.
+ MESSAGE
+ end
+
+ expect(removed_workers).to be_empty, message
+ end
+
it 'uses the default number of retries for new jobs' do
expect(workers_without_defaults - cronjobs - retry_exception_workers).to all(have_attributes(retries: true))
end
diff --git a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
index 2945bcbe641..e385a5aaf3f 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
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_cat
let(:stage_enabled) { true }
before do
- settings.write({ attachments_import: stage_enabled })
+ settings.write({ optional_stages: { attachments_import: stage_enabled } })
end
describe '#import' do
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
index 33ecf848997..808f6e827ed 100644
--- a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_c
let(:push_rights_granted) { true }
before do
- settings.write({ collaborators_import: stage_enabled })
+ settings.write({ optional_stages: { collaborators_import: stage_enabled } })
allow(client).to receive(:repository).with(project.import_source)
.and_return({ permissions: { push: push_rights_granted } })
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 c70ee0250e8..7b0cf77bbbe 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
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_cat
let(:stage_enabled) { true }
before do
- settings.write({ single_endpoint_issue_events_import: stage_enabled })
+ settings.write({ optional_stages: { single_endpoint_issue_events_import: stage_enabled } })
end
describe '#import' do
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 872201ece93..188cf3530f7 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
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker, feat
let(:single_endpoint_optional_stage) { true }
before do
- settings.write({ single_endpoint_notes_import: single_endpoint_optional_stage })
+ settings.write({ optional_stages: { single_endpoint_notes_import: single_endpoint_optional_stage } })
end
describe '#import' do
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 8c0004c0f3f..dcceeb1d6c2 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
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker, feature_category:
let(:single_endpoint_optional_stage) { true }
before do
- settings.write({ single_endpoint_notes_import: single_endpoint_optional_stage })
+ settings.write({ optional_stages: { single_endpoint_notes_import: single_endpoint_optional_stage } })
end
describe '#import' do
diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb
index 3c2e69a4675..02ca5260fbd 100644
--- a/spec/workers/incident_management/close_incident_worker_spec.rb
+++ b/spec/workers/incident_management/close_incident_worker_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe IncidentManagement::CloseIncidentWorker, feature_category: :incid
context 'when issue type is not incident' do
before do
- issue.update!(issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:issue))
+ issue.update!(work_item_type: WorkItems::Type.default_by_type(:issue))
end
it_behaves_like 'does not call the close issue service'
diff --git a/spec/workers/integrations/execute_worker_spec.rb b/spec/workers/integrations/execute_worker_spec.rb
index 717e3c65820..369fc5fd091 100644
--- a/spec/workers/integrations/execute_worker_spec.rb
+++ b/spec/workers/integrations/execute_worker_spec.rb
@@ -36,4 +36,18 @@ RSpec.describe Integrations::ExecuteWorker, '#perform', feature_category: :integ
end.not_to raise_error
end
end
+
+ context 'when the Gitlab::SilentMode is enabled' do
+ before do
+ allow(Gitlab::SilentMode).to receive(:enabled?).and_return(true)
+ end
+
+ it 'completes silently and does not log an error' do
+ expect(Gitlab::IntegrationsLogger).not_to receive(:error)
+
+ expect do
+ worker.perform(non_existing_record_id, {})
+ end.not_to raise_error
+ end
+ end
end
diff --git a/spec/workers/integrations/group_mention_worker_spec.rb b/spec/workers/integrations/group_mention_worker_spec.rb
new file mode 100644
index 00000000000..111e3f5a107
--- /dev/null
+++ b/spec/workers/integrations/group_mention_worker_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GroupMentionWorker, :clean_gitlab_redis_shared_state, feature_category: :integrations do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:service_class) { Integrations::GroupMentionService }
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:issue) { create(:issue, confidential: false, project: project, author: user) }
+ let(:hook_data) { issue.to_hook_data(user) }
+ let(:is_confidential) { issue.confidential? }
+
+ let(:args) do
+ {
+ mentionable_type: 'Issue',
+ mentionable_id: issue.id,
+ hook_data: hook_data,
+ is_confidential: is_confidential
+ }
+ end
+
+ it 'executes the service' do
+ expect_next_instance_of(service_class, issue, hook_data: hook_data, is_confidential: is_confidential) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ worker.perform(args)
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [args] }
+ end
+
+ context 'when mentionable_type is not supported' do
+ let(:args) do
+ {
+ mentionable_type: 'Unsupported',
+ mentionable_id: 23,
+ hook_data: {},
+ is_confidential: false
+ }
+ end
+
+ it 'does not execute the service' do
+ expect(service_class).not_to receive(:new)
+
+ worker.perform(args)
+ end
+
+ it 'logs an error' do
+ expect(Sidekiq.logger).to receive(:error).with({
+ message: 'Integrations::GroupMentionWorker: mentionable not supported',
+ mentionable_type: 'Unsupported',
+ mentionable_id: 23
+ })
+
+ worker.perform(args)
+ end
+ end
+ end
+end
diff --git a/spec/workers/jira_connect/retry_request_worker_spec.rb b/spec/workers/jira_connect/retry_request_worker_spec.rb
index e96a050da13..5453711f24e 100644
--- a/spec/workers/jira_connect/retry_request_worker_spec.rb
+++ b/spec/workers/jira_connect/retry_request_worker_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe JiraConnect::RetryRequestWorker, feature_category: :integrations
subject(:perform) { described_class.new.perform(event_url, jwt, attempts) }
it 'sends the request, with the appropriate headers' do
- expect(JiraConnect::RetryRequestWorker).not_to receive(:perform_in)
+ expect(described_class).not_to receive(:perform_in)
stub_request(:post, event_url)
@@ -26,7 +26,7 @@ RSpec.describe JiraConnect::RetryRequestWorker, feature_category: :integrations
end
it 'arranges to retry the request' do
- expect(JiraConnect::RetryRequestWorker).to receive(:perform_in).with(1.hour, event_url, jwt, attempts - 1)
+ expect(described_class).to receive(:perform_in).with(1.hour, event_url, jwt, attempts - 1)
perform
end
@@ -35,7 +35,7 @@ RSpec.describe JiraConnect::RetryRequestWorker, feature_category: :integrations
let(:attempts) { 0 }
it 'does not retry' do
- expect(JiraConnect::RetryRequestWorker).not_to receive(:perform_in)
+ expect(described_class).not_to receive(:perform_in)
perform
end
diff --git a/spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb b/spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb
index 2c429ed62fb..828ffb0c811 100644
--- a/spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb
+++ b/spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe MergeRequests::MergeabilityCheckBatchWorker, feature_category: :code_review_workflow do
subject { described_class.new }
+ let_it_be(:user) { create(:user) }
+
describe '#perform' do
context 'when some merge_requests do not exist' do
it 'ignores unknown merge request ids' do
@@ -12,26 +14,49 @@ RSpec.describe MergeRequests::MergeabilityCheckBatchWorker, feature_category: :c
expect(Sidekiq.logger).not_to receive(:error)
- subject.perform([1234, 5678])
+ subject.perform([1234, 5678], user.id)
end
end
context 'when some merge_requests needs mergeability checks' do
let(:merge_request_1) { create(:merge_request, merge_status: :unchecked) }
- let(:merge_request_2) { create(:merge_request, merge_status: :cannot_be_merged_rechecking) }
+ let(:merge_request_2) { create(:merge_request, merge_status: :unchecked) }
let(:merge_request_3) { create(:merge_request, merge_status: :can_be_merged) }
+ before do
+ merge_request_1.project.add_developer(user)
+ merge_request_2.project.add_reporter(user)
+ merge_request_3.project.add_developer(user)
+ end
+
it 'executes MergeabilityCheckService on merge requests that needs to be checked' do
expect_next_instance_of(MergeRequests::MergeabilityCheckService, merge_request_1) do |service|
expect(service).to receive(:execute).and_return(ServiceResponse.success)
end
- expect_next_instance_of(MergeRequests::MergeabilityCheckService, merge_request_2) do |service|
- expect(service).to receive(:execute).and_return(ServiceResponse.success)
- end
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(merge_request_2.id)
expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(merge_request_3.id)
expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(1234)
- subject.perform([merge_request_1.id, merge_request_2.id, merge_request_3.id, 1234])
+ subject.perform([merge_request_1.id, merge_request_2.id, merge_request_3.id, 1234], user.id)
+ end
+
+ context 'when restrict_merge_status_recheck FF is off' do
+ before do
+ stub_feature_flags(restrict_merge_status_recheck: false)
+ end
+
+ it 'executes MergeabilityCheckService on merge requests that needs to be checked' do
+ expect_next_instance_of(MergeRequests::MergeabilityCheckService, merge_request_1) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+ expect_next_instance_of(MergeRequests::MergeabilityCheckService, merge_request_2) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(merge_request_3.id)
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(1234)
+
+ subject.perform([merge_request_1.id, merge_request_2.id, merge_request_3.id, 1234], user.id)
+ end
end
it 'structurally logs a failed mergeability check' do
@@ -45,13 +70,26 @@ RSpec.describe MergeRequests::MergeabilityCheckBatchWorker, feature_category: :c
worker: described_class.to_s,
message: 'Failed to check mergeability of merge request: solar flares')
- subject.perform([merge_request_1.id])
+ subject.perform([merge_request_1.id], user.id)
+ end
+
+ context 'when user is nil' do
+ let(:user) { nil }
+
+ it 'does not run any mergeability checks' do
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(merge_request_1.id)
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(merge_request_2.id)
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(merge_request_3.id)
+ expect(MergeRequests::MergeabilityCheckService).not_to receive(:new).with(1234)
+
+ subject.perform([merge_request_1.id, merge_request_2.id, merge_request_3.id, 1234], user&.id)
+ end
end
end
it_behaves_like 'an idempotent worker' do
let(:merge_request) { create(:merge_request) }
- let(:job_args) { [merge_request.id] }
+ let(:job_args) { [[merge_request.id], user.id] }
it 'is mergeable' do
subject
diff --git a/spec/workers/packages/debian/process_changes_worker_spec.rb b/spec/workers/packages/debian/process_changes_worker_spec.rb
deleted file mode 100644
index 435ca33b223..00000000000
--- a/spec/workers/packages/debian/process_changes_worker_spec.rb
+++ /dev/null
@@ -1,133 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker, feature_category: :package_registry do
- let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:distribution) do
- create(:debian_project_distribution, :with_file, codename: FFaker::Lorem.word, suite: 'unstable')
- end
-
- let(:incoming) { create(:debian_incoming, project: distribution.project, with_changes_file: true) }
- let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2_amd64.changes').first }
- let(:worker) { described_class.new }
-
- describe '#perform' do
- let(:package_file_id) { package_file.id }
- let(:user_id) { user.id }
-
- subject { worker.perform(package_file_id, user_id) }
-
- context 'with mocked service' do
- it 'calls ProcessChangesService' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect_next_instance_of(::Packages::Debian::ProcessChangesService) do |service|
- expect(service).to receive(:execute)
- .with(no_args)
- end
-
- subject
- end
- end
-
- context 'with non existing package file' do
- let(:package_file_id) { non_existing_record_id }
-
- it 'returns early without error' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect(::Packages::Debian::ProcessChangesService).not_to receive(:new)
-
- subject
- end
- end
-
- context 'with nil package file id' do
- let(:package_file_id) { nil }
-
- it 'returns early without error' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect(::Packages::Debian::ProcessChangesService).not_to receive(:new)
-
- subject
- end
- end
-
- context 'with non existing user' do
- let(:user_id) { non_existing_record_id }
-
- it 'returns early without error' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect(::Packages::Debian::ProcessChangesService).not_to receive(:new)
-
- subject
- end
- end
-
- context 'with nil user id' do
- let(:user_id) { nil }
-
- it 'returns early without error' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect(::Packages::Debian::ProcessChangesService).not_to receive(:new)
-
- subject
- end
- end
-
- context 'without a distribution' do
- before do
- distribution.destroy!
- end
-
- it 'removes package file and log exception', :aggregate_failures do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
- instance_of(ActiveRecord::RecordNotFound),
- package_file_id: package_file_id,
- user_id: user_id
- )
- expect { subject }
- .to not_change { Packages::Package.count }
- .and change { Packages::PackageFile.count }.by(-1)
- .and change { incoming.package_files.count }.from(8).to(7)
- end
- end
-
- context 'when the service raises an error' do
- let(:package_file) { incoming.package_files.first }
-
- it 'removes package file and log exception', :aggregate_failures do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
- instance_of(Packages::Debian::ExtractChangesMetadataService::ExtractionError),
- package_file_id: package_file_id,
- user_id: user_id
- )
- expect { subject }
- .to not_change { Packages::Package.count }
- .and change { Packages::PackageFile.count }.by(-1)
- .and change { incoming.package_files.count }.from(8).to(7)
-
- expect { package_file.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
-
- it_behaves_like 'an idempotent worker' do
- let(:job_args) { [package_file.id, user.id] }
-
- it 'sets the Debian file type as changes', :aggregate_failures do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- # Using subject inside this block will process the job multiple times
- expect { subject }
- .to change { Packages::Package.count }.from(1).to(2)
- .and not_change { Packages::PackageFile.count }
- .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
- expect(created_package.name).to eq 'sample'
- expect(created_package.version).to eq '1.2.3~alpha2'
- expect(created_package.creator).to eq user
- end
- end
- end
-end
diff --git a/spec/workers/packages/nuget/extraction_worker_spec.rb b/spec/workers/packages/nuget/extraction_worker_spec.rb
index c1d42d556c2..11eaa1b5dde 100644
--- a/spec/workers/packages/nuget/extraction_worker_spec.rb
+++ b/spec/workers/packages/nuget/extraction_worker_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker, feature_categor
allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
end
- it_behaves_like 'handling the metadata error', exception_class: ::Packages::Nuget::MetadataExtractionService::ExtractionError
+ it_behaves_like 'handling the metadata error', exception_class: ::Packages::Nuget::ExtractMetadataFileService::ExtractionError
end
context 'with package with an invalid package name' do
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index da6a0254a17..48138034c33 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -77,8 +77,18 @@ RSpec.describe PipelineScheduleWorker, :sidekiq_inline, feature_category: :conti
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
end
- it 'does not creates a new pipeline' do
- expect { subject }.not_to change { project.ci_pipelines.count }
+ it 'creates a new pipeline' do
+ expect { subject }.to change { project.ci_pipelines.count }.by(1)
+ end
+
+ context 'with feature flag persist_failed_pipelines_from_schedules disabled' do
+ before do
+ stub_feature_flags(persist_failed_pipelines_from_schedules: false)
+ end
+
+ it 'does not create a new pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
+ end
end
end
end
diff --git a/spec/workers/redis_migration_worker_spec.rb b/spec/workers/redis_migration_worker_spec.rb
new file mode 100644
index 00000000000..ad0186e929d
--- /dev/null
+++ b/spec/workers/redis_migration_worker_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RedisMigrationWorker, :clean_gitlab_redis_shared_state, feature_category: :redis do
+ describe '.fetch_migrator!' do
+ it 'raise error if class does not exist' do
+ expect { described_class.fetch_migrator!('UnknownClass') }.to raise_error(NotImplementedError)
+ end
+
+ context 'when class exists' do
+ it 'returns an instance' do
+ expect(
+ described_class.fetch_migrator!('BackfillProjectPipelineStatusTtl')
+ ).to be_a Gitlab::BackgroundMigration::Redis::BackfillProjectPipelineStatusTtl
+ end
+ end
+ end
+
+ describe '#perform' do
+ let(:job_class_name) { 'SampleJob' }
+ let(:migrator_class) do
+ Class.new do
+ def perform(keys)
+ keys.each { |key| redis.set(key, "adjusted", ex: 10) }
+ end
+
+ def scan_match_pattern
+ 'sample:*:pattern'
+ end
+
+ def redis
+ ::Redis.new(::Gitlab::Redis::Cache.params)
+ end
+ end
+ end
+
+ let(:migrator) { migrator_class.new }
+
+ before do
+ allow(described_class).to receive(:fetch_migrator!).with(job_class_name).and_return(migrator)
+
+ 100.times do |i|
+ migrator.redis.set("sample:#{i}:pattern", i)
+ end
+ end
+
+ it 'runs migration logic on scanned keys' do
+ expect(migrator).to receive(:perform)
+
+ subject.perform(job_class_name, '0')
+ end
+
+ context 'when job exceeds deadline' do
+ before do
+ # stub Time.now to force the 3rd invocation to timeout
+ now = Time.now # rubocop:disable Rails/TimeZone
+ allow(Time).to receive(:now).and_return(now, now, now + 5.minutes)
+ end
+
+ it 'enqueues another job and returns' do
+ expect(described_class).to receive(:perform_async)
+
+ # use smaller scan_size to ensure multiple scans are required
+ subject.perform(job_class_name, '0', { scan_size: 10 })
+ end
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [job_class_name, '0'] }
+ end
+ end
+end
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index d0e4de1aa98..4b46ca1e125 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
before do
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
- expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
+ expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: pipeline_schedule).and_return(service_response)
end
context "when pipeline is persisted" do
@@ -124,7 +124,26 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
it 'creates a pipeline' do
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
- expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
+ expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: pipeline_schedule).and_return(service_response)
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+ end
+
+ context 'with feature flag persist_failed_pipelines_from_schedules disabled' do
+ before do
+ stub_feature_flags(persist_failed_pipelines_from_schedules: false)
+ end
+
+ it 'does not save_on_errors' do
+ expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
+
+ expect(create_pipeline_service).to receive(:execute).with(
+ :schedule,
+ ignore_skip_ci: true,
+ save_on_errors: false,
+ schedule: pipeline_schedule
+ )
worker.perform(pipeline_schedule.id, user.id)
end