From d298fad0c0564454271cba11e6f20c19681534ac Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 5 Feb 2021 16:20:45 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-9-stable-ee --- spec/benchmarks/banzai_benchmark.rb | 114 ++++ spec/config/object_store_settings_spec.rb | 69 ++- spec/controllers/admin/cohorts_controller_spec.rb | 34 +- spec/controllers/admin/runners_controller_spec.rb | 3 +- spec/controllers/admin/users_controller_spec.rb | 7 +- spec/controllers/chaos_controller_spec.rb | 19 + .../controllers/concerns/spammable_actions_spec.rb | 99 +-- ...endency_proxy_for_containers_controller_spec.rb | 16 +- .../groups/group_members_controller_spec.rb | 12 + spec/controllers/groups_controller_spec.rb | 60 -- .../import/bulk_imports_controller_spec.rb | 20 +- spec/controllers/invites_controller_spec.rb | 43 +- .../projects/discussions_controller_spec.rb | 7 + spec/controllers/projects/forks_controller_spec.rb | 22 + .../controllers/projects/issues_controller_spec.rb | 57 +- .../merge_requests/diffs_controller_spec.rb | 8 +- .../projects/merge_requests_controller_spec.rb | 102 +++ spec/controllers/projects/notes_controller_spec.rb | 4 +- .../projects/pipelines/tests_controller_spec.rb | 24 +- .../projects/project_members_controller_spec.rb | 12 + .../security/configuration_controller_spec.rb | 53 ++ .../experience_levels_controller_spec.rb | 107 ++-- spec/controllers/registrations_controller_spec.rb | 12 + .../repositories/git_http_controller_spec.rb | 8 +- spec/controllers/search_controller_spec.rb | 392 ++++++------ spec/db/schema_spec.rb | 1 + spec/deprecation_toolkit_env.rb | 99 ++- spec/experiments/application_experiment_spec.rb | 80 ++- .../members/invite_email_experiment_spec.rb | 41 ++ spec/factories/audit_events.rb | 15 + spec/factories/ci/bridge.rb | 9 + spec/factories/ci/builds.rb | 6 - spec/factories/ci/pipeline_artifacts.rb | 33 +- spec/factories/ci/pipelines.rb | 16 +- .../ci/reports/codequality_degradations.rb | 98 +++ spec/factories/ci/resource.rb | 2 +- spec/factories/dependency_proxy.rb | 3 +- spec/factories/merge_request_diffs.rb | 6 + spec/factories/merge_requests.rb | 12 + spec/factories/packages.rb | 18 + spec/factories/packages/debian/group_component.rb | 9 + .../factories/packages/debian/project_component.rb | 9 + spec/factories/token_with_ivs.rb | 9 + spec/factories/u2f_registrations.rb | 2 + spec/features/admin/admin_cohorts_spec.rb | 33 - .../admin_disables_git_access_protocol_spec.rb | 15 +- spec/features/admin/admin_groups_spec.rb | 2 +- spec/features/admin/admin_projects_spec.rb | 3 + spec/features/admin/admin_settings_spec.rb | 73 ++- spec/features/admin/admin_users_spec.rb | 70 +++ .../alert_management/alert_details_spec.rb | 2 +- .../user_views_alerts_settings_spec.rb | 3 +- spec/features/boards/boards_spec.rb | 2 + spec/features/boards/sidebar_spec.rb | 8 +- spec/features/commits_spec.rb | 3 +- spec/features/discussion_comments/issue_spec.rb | 2 + .../discussion_comments/merge_request_spec.rb | 1 + spec/features/discussion_comments/snippets_spec.rb | 29 +- .../groups/import_export/connect_instance_spec.rb | 16 +- spec/features/groups/navbar_spec.rb | 8 - .../settings/packages_and_registries_spec.rb | 7 + spec/features/groups/show_spec.rb | 1 + spec/features/issuables/issuable_list_spec.rb | 2 +- spec/features/issues/gfm_autocomplete_spec.rb | 10 + spec/features/issues/issue_sidebar_spec.rb | 35 ++ spec/features/issues/issue_state_spec.rb | 26 +- .../issues/user_toggles_subscription_spec.rb | 6 +- spec/features/labels_hierarchy_spec.rb | 5 + spec/features/markdown/mermaid_spec.rb | 2 +- ...user_closes_reopens_merge_request_state_spec.rb | 37 +- .../user_manages_subscription_spec.rb | 2 +- .../user_merges_only_if_pipeline_succeeds_spec.rb | 4 +- .../user_merges_when_pipeline_succeeds_spec.rb | 8 +- .../merge_request/user_posts_notes_spec.rb | 2 +- .../user_reverts_merge_request_spec.rb | 41 +- .../user_sees_merge_request_pipelines_spec.rb | 4 +- .../merge_request/user_sees_merge_widget_spec.rb | 4 +- .../user_sees_mini_pipeline_graph_spec.rb | 234 +++---- .../user_suggests_changes_on_diff_spec.rb | 17 + .../user_views_open_merge_request_spec.rb | 16 + .../profiles/user_visits_notifications_tab_spec.rb | 1 + spec/features/projects/commit/cherry_pick_spec.rb | 160 ++--- .../projects/commit/user_reverts_commit_spec.rb | 91 ++- .../projects/commits/user_browses_commits_spec.rb | 9 +- spec/features/projects/compare_spec.rb | 23 +- .../projects/files/dockerfile_dropdown_spec.rb | 8 +- .../projects/files/gitignore_dropdown_spec.rb | 10 +- .../files/gitlab_ci_syntax_yml_dropdown_spec.rb | 7 +- .../projects/files/gitlab_ci_yml_dropdown_spec.rb | 10 +- spec/features/projects/jobs_spec.rb | 16 +- .../members/anonymous_user_sees_members_spec.rb | 2 + .../projects/members/group_members_spec.rb | 2 + .../members/groups_with_access_list_spec.rb | 2 + .../features/projects/members/invite_group_spec.rb | 5 + spec/features/projects/members/list_spec.rb | 8 + ...master_adds_member_with_expiration_date_spec.rb | 4 + spec/features/projects/members/sorting_spec.rb | 2 + spec/features/projects/members/tabs_spec.rb | 1 + spec/features/projects/navbar_spec.rb | 19 - .../projects/pages/user_adds_domain_spec.rb | 185 ++++++ .../pages/user_edits_lets_encrypt_settings_spec.rb | 167 +++++ .../projects/pages/user_edits_settings_spec.rb | 201 ++++++ spec/features/projects/pages_lets_encrypt_spec.rb | 167 ----- spec/features/projects/pages_spec.rb | 411 ------------- spec/features/projects/pipelines/pipeline_spec.rb | 15 +- spec/features/projects/pipelines/pipelines_spec.rb | 106 ++-- .../user_activates_slack_slash_command_spec.rb | 4 + .../projects/settings/pipelines_settings_spec.rb | 2 +- .../projects/settings/repository_settings_spec.rb | 6 +- .../settings/user_manages_project_members_spec.rb | 4 + .../show/user_manages_notifications_spec.rb | 1 + .../show/user_sees_git_instructions_spec.rb | 7 + .../search/user_searches_for_commits_spec.rb | 4 +- .../search/user_searches_for_issues_spec.rb | 2 +- .../user_searches_for_merge_requests_spec.rb | 34 +- .../search/user_searches_for_projects_spec.rb | 2 +- spec/features/task_lists_spec.rb | 8 +- spec/features/u2f_spec.rb | 10 + spec/features/user_sees_revert_modal_spec.rb | 27 +- spec/features/webauthn_spec.rb | 4 + spec/features/whats_new_spec.rb | 35 ++ spec/finders/autocomplete/users_finder_spec.rb | 5 + spec/finders/merge_request/metrics_finder_spec.rb | 76 +++ .../oldest_per_commit_finder_spec.rb | 46 ++ .../commits_with_trailer_finder_spec.rb | 38 ++ spec/finders/terraform/states_finder_spec.rb | 45 ++ spec/finders/user_recent_events_finder_spec.rb | 14 + .../entities/codequality_mr_diff_report.json | 21 + .../api/schemas/entities/group_group_link.json | 40 -- spec/fixtures/api/schemas/entities/member.json | 4 +- .../fixtures/api/schemas/entities/member_user.json | 1 + .../graphql/packages/package_composer_details.json | 12 - .../packages/package_composer_metadata.json | 21 + .../schemas/graphql/packages/package_details.json | 38 +- spec/fixtures/api/schemas/group_group_links.json | 6 - .../api/schemas/group_link/group_group_link.json | 16 + .../api/schemas/group_link/group_group_links.json | 6 + .../api/schemas/group_link/group_link.json | 38 ++ .../api/schemas/group_link/project_group_link.json | 16 + .../schemas/group_link/project_group_links.json | 6 + spec/fixtures/dependency_proxy/manifest | 44 +- spec/fixtures/markdown.md.erb | 8 + spec/fixtures/packages/composer/package.json | 1 + .../pipeline_artifacts/code_quality_mr_diff.json | 23 + spec/fixtures/whats_new/invalid.yml | 2 +- spec/fixtures/whats_new/valid.yml | 2 +- spec/frontend/__helpers__/graphql_helpers.js | 14 + spec/frontend/__helpers__/graphql_helpers_spec.js | 23 + spec/frontend/__helpers__/stub_component.js | 25 + .../components/add_context_commits_modal_spec.js | 2 +- .../admin/users/components/user_actions_spec.js | 138 +++++ .../admin/users/components/user_avatar_spec.js | 64 +- .../admin/users/components/user_date_spec.js | 34 + .../admin/users/components/users_table_spec.js | 26 +- spec/frontend/admin/users/index_spec.js | 4 +- spec/frontend/admin/users/mock_data.js | 1 + .../components/alert_details_spec.js | 338 ---------- .../alert_management_sidebar_todo_spec.js | 112 ---- .../components/alert_management_table_spec.js | 2 +- .../components/alert_metrics_spec.js | 62 -- .../components/alert_status_spec.js | 151 ----- .../components/alert_summary_row_spec.js | 40 -- .../alert_managment_sidebar_assignees_spec.js | 173 ------ .../components/sidebar/alert_sidebar_spec.js | 64 -- .../sidebar/alert_sidebar_status_spec.js | 130 ---- .../alert_management_system_note_spec.js | 40 -- spec/frontend/alert_management/mocks/alerts.json | 71 --- .../alerts_settings_form_spec.js.snap | 98 --- .../alerts_settings/alert_mapping_builder_spec.js | 93 --- .../alerts_integrations_list_spec.js | 118 ---- .../alerts_settings/alerts_settings_form_spec.js | 351 ----------- .../alerts_settings_wrapper_spec.js | 379 ------------ .../alerts_settings_form_spec.js.snap | 406 ++++++++++++ .../components/alert_mapping_builder_spec.js | 102 +++ .../components/alerts_integrations_list_spec.js | 118 ++++ .../components/alerts_settings_form_spec.js | 385 ++++++++++++ .../components/alerts_settings_wrapper_spec.js | 379 ++++++++++++ .../components/mocks/apollo_mock.js | 123 ++++ .../components/mocks/integrations.json | 38 ++ spec/frontend/alerts_settings/components/util.js | 24 + .../alerts_settings/mocks/alertFields.json | 123 ++++ spec/frontend/alerts_settings/mocks/apollo_mock.js | 123 ---- .../alerts_settings/mocks/integrations.json | 38 -- spec/frontend/alerts_settings/util.js | 24 - .../utils/mapping_transformations_spec.js | 81 +++ spec/frontend/api/api_utils_spec.js | 4 + spec/frontend/api_spec.js | 22 + spec/frontend/behaviors/autosize_spec.js | 32 +- .../__snapshots__/blob_header_spec.js.snap | 2 +- spec/frontend/blob/components/blob_content_spec.js | 2 +- .../blob/components/blob_header_filepath_spec.js | 2 +- .../components/popover_spec.js | 2 +- spec/frontend/boards/board_list_deprecated_spec.js | 2 +- spec/frontend/boards/board_list_helper.js | 2 +- spec/frontend/boards/board_list_spec.js | 4 +- spec/frontend/boards/boards_store_spec.js | 2 +- spec/frontend/boards/boards_util_spec.js | 17 + .../components/board_assignee_dropdown_spec.js | 17 +- .../board_card_layout_deprecated_spec.js | 158 +++++ .../boards/components/board_card_layout_spec.js | 65 +- .../components/board_configuration_options_spec.js | 15 + .../boards/components/board_content_spec.js | 2 +- spec/frontend/boards/components/board_form_spec.js | 2 +- .../board_list_header_deprecated_spec.js | 8 +- .../boards/components/board_list_header_spec.js | 10 +- .../sidebar/board_sidebar_milestone_select_spec.js | 14 +- .../sidebar/board_sidebar_subscription_spec.js | 2 +- spec/frontend/boards/issue_card_deprecated_spec.js | 2 +- spec/frontend/boards/issue_card_inner_spec.js | 2 +- spec/frontend/boards/issue_spec.js | 2 +- spec/frontend/boards/mock_data.js | 4 +- spec/frontend/boards/stores/actions_spec.js | 81 ++- spec/frontend/boards/stores/getters_spec.js | 18 +- spec/frontend/boards/stores/mutations_spec.js | 42 +- .../frontend/captcha/init_recaptcha_script_spec.js | 49 ++ .../ci_variable_list/store/actions_spec.js | 2 +- .../remove_cluster_confirmation_spec.js.snap | 8 +- .../clusters/components/applications_spec.js | 58 +- spec/frontend/clusters_list/store/actions_spec.js | 2 +- .../frontend/clusters_list/store/mutations_spec.js | 2 +- .../eks_cluster/store/actions_spec.js | 2 +- .../components/gke_machine_type_dropdown_spec.js | 2 +- .../components/gke_project_id_dropdown_spec.js | 2 +- spec/frontend/deploy_keys/components/key_spec.js | 2 +- .../design_notes/design_discussion_spec.js | 2 +- .../components/design_overlay_spec.js | 2 +- .../components/design_sidebar_spec.js | 2 +- .../components/design_todo_button_spec.js | 2 +- .../list/__snapshots__/item_spec.js.snap | 10 +- .../design_management/components/list/item_spec.js | 50 +- .../design_management/pages/design/index_spec.js | 8 +- .../frontend/design_management/pages/index_spec.js | 10 +- .../design_management/utils/cache_update_spec.js | 2 +- spec/frontend/diffs/components/app_spec.js | 6 +- .../diffs/components/compare_versions_spec.js | 2 +- .../frontend/diffs/components/diff_content_spec.js | 2 +- .../diffs/components/diff_file_header_spec.js | 296 +++++++-- spec/frontend/diffs/components/diff_file_spec.js | 63 +- spec/frontend/diffs/components/diff_row_spec.js | 2 +- .../diffs/components/diff_row_utils_spec.js | 11 + spec/frontend/diffs/components/diff_view_spec.js | 12 +- .../diffs/components/inline_diff_table_row_spec.js | 2 +- .../components/parallel_diff_table_row_spec.js | 2 +- spec/frontend/diffs/store/actions_spec.js | 2 +- spec/frontend/diffs/store/getters_spec.js | 22 - spec/frontend/diffs/store/mutations_spec.js | 2 +- spec/frontend/diffs/utils/diff_file_spec.js | 13 +- spec/frontend/diffs/utils/file_reviews_spec.js | 48 +- spec/frontend/editor/editor_lite_spec.js | 298 ++++++--- .../environments/environment_actions_spec.js | 2 +- .../components/error_tracking_list_spec.js | 2 +- .../components/edit_feature_flag_spec.js | 9 +- .../frontend/feature_flags/components/form_spec.js | 43 +- .../components/new_feature_flag_spec.js | 12 +- .../filtered_search/recent_searches_root_spec.js | 49 +- .../frontend/frequent_items/components/app_spec.js | 2 +- .../components/frequent_items_search_input_spec.js | 34 - spec/frontend/frequent_items/store/actions_spec.js | 2 +- spec/frontend/gfm_auto_complete_spec.js | 13 +- .../__snapshots__/grafana_integration_spec.js.snap | 6 +- spec/frontend/groups/components/app_spec.js | 4 +- .../groups/components/group_folder_spec.js | 2 +- spec/frontend/groups/components/group_item_spec.js | 2 +- spec/frontend/groups/components/groups_spec.js | 4 +- .../groups/components/item_actions_spec.js | 16 + .../frontend/groups/members/components/app_spec.js | 95 --- spec/frontend/groups/members/index_spec.js | 113 ---- spec/frontend/groups/members/mock_data.js | 33 - spec/frontend/groups/members/utils_spec.js | 45 +- spec/frontend/ide/components/activity_bar_spec.js | 17 + .../ide/components/commit_sidebar/actions_spec.js | 25 +- .../ide/components/commit_sidebar/form_spec.js | 391 ++++++------ .../new_merge_request_option_spec.js | 9 +- .../ide/components/ide_sidebar_nav_spec.js | 3 +- spec/frontend/ide/components/ide_spec.js | 56 +- .../ide/components/preview/navigator_spec.js | 2 +- .../ide/lib/decorations/controller_spec.js | 2 +- spec/frontend/ide/lib/editor_spec.js | 3 +- spec/frontend/ide/stores/actions_spec.js | 2 +- spec/frontend/ide/stores/getters_spec.js | 5 +- .../ide/stores/modules/commit/actions_spec.js | 17 +- .../ide/stores/modules/commit/getters_spec.js | 9 +- .../import_groups/components/import_table_spec.js | 156 ++++- .../import_groups/graphql/client_factory_spec.js | 103 ++-- .../graphql/services/status_poller_spec.js | 88 +-- .../incidents_settings_tabs_spec.js.snap | 4 +- .../components/pagerduty_form_spec.js | 2 +- .../edit/components/integration_form_spec.js | 157 +++-- .../edit/components/jira_issues_fields_spec.js | 69 ++- .../integrations/edit/store/actions_spec.js | 33 + .../integrations/edit/store/mutations_spec.js | 26 + .../frontend/integrations/edit/store/state_spec.js | 3 + .../integrations/integration_settings_form_spec.js | 79 +++ .../issuable_list/components/issuable_item_spec.js | 17 + spec/frontend/issue_show/components/app_spec.js | 8 +- .../components/incidents/incident_tabs_spec.js | 2 +- spec/frontend/issue_show/issue_spec.js | 2 +- .../issues_list/components/issuable_spec.js | 2 +- spec/frontend/jira_connect/api_spec.js | 2 +- spec/frontend/jira_connect/components/app_spec.js | 36 +- .../components/groups_list_item_spec.js | 109 +++- .../jira_connect/components/groups_list_spec.js | 30 +- spec/frontend/jira_connect/index_spec.js | 56 ++ spec/frontend/jira_connect/mock_data.js | 2 + .../components/jira_import_form_spec.js | 79 ++- spec/frontend/jobs/components/erased_block_spec.js | 4 +- spec/frontend/jobs/components/job_app_spec.js | 1 - .../job_sidebar_details_container_spec.js | 9 - .../components/job_sidebar_retry_button_spec.js | 2 +- .../frontend/jobs/components/trigger_block_spec.js | 116 ++-- spec/frontend/lib/utils/array_utility_spec.js | 32 + spec/frontend/lib/utils/color_utils_spec.js | 17 +- spec/frontend/lib/utils/common_utils_spec.js | 8 + spec/frontend/lib/utils/datetime_utility_spec.js | 343 ++++++++--- .../utils/unit_format/formatter_factory_spec.js | 21 + .../logs/components/log_advanced_filters_spec.js | 3 +- .../logs/components/log_simple_filters_spec.js | 3 +- spec/frontend/logs/stores/actions_spec.js | 2 +- spec/frontend/members/components/app_spec.js | 95 +++ .../components/avatars/group_avatar_spec.js | 2 +- .../components/avatars/invite_avatar_spec.js | 2 +- .../members/components/avatars/user_avatar_spec.js | 2 +- .../components/table/member_action_buttons_spec.js | 2 +- .../members/components/table/member_avatar_spec.js | 2 +- .../components/table/members_table_cell_spec.js | 22 +- .../members/components/table/members_table_spec.js | 9 +- .../members/components/table/role_dropdown_spec.js | 3 +- spec/frontend/members/index_spec.js | 113 ++++ spec/frontend/members/mock_data.js | 6 + spec/frontend/members/store/actions_spec.js | 12 +- spec/frontend/members/store/mutations_spec.js | 64 +- spec/frontend/members/utils_spec.js | 98 ++- .../merge_request/components/status_box_spec.js | 6 + .../frontend/milestones/milestone_combobox_spec.js | 2 +- .../monitoring/components/charts/anomaly_spec.js | 2 +- .../components/charts/single_stat_spec.js | 11 +- .../components/charts/time_series_spec.js | 4 +- .../components/dashboard_actions_menu_spec.js | 4 +- .../monitoring/components/dashboard_header_spec.js | 2 +- .../components/dashboard_panel_builder_spec.js | 5 +- .../monitoring/components/dashboard_panel_spec.js | 32 +- .../monitoring/components/dashboard_spec.js | 2 +- .../components/dashboard_url_time_spec.js | 4 +- spec/frontend/monitoring/csv_export_spec.js | 2 +- spec/frontend/monitoring/mock_data.js | 2 +- spec/frontend/monitoring/requests/index_spec.js | 2 +- spec/frontend/monitoring/store/utils_spec.js | 2 +- .../monitoring/store/variable_mapping_spec.js | 2 +- .../frontend/notes/components/comment_form_spec.js | 9 - .../notes/components/discussion_actions_spec.js | 2 +- .../notes/components/discussion_counter_spec.js | 2 +- .../frontend/notes/components/note_actions_spec.js | 7 +- spec/frontend/notes/components/note_form_spec.js | 28 +- spec/frontend/notes/components/notes_app_spec.js | 2 +- spec/frontend/notes/stores/actions_spec.js | 63 +- spec/frontend/notes/stores/mutation_spec.js | 13 + .../components/notifications_dropdown_spec.js | 240 ++++++++ spec/frontend/onboarding_issues/index_spec.js | 137 ----- .../packages/details/store/getters_spec.js | 2 +- .../__snapshots__/packages_filter_spec.js.snap | 14 - .../__snapshots__/packages_list_app_spec.js.snap | 555 ++--------------- .../list/components/packages_filter_spec.js | 50 -- .../list/components/packages_list_app_spec.js | 49 +- .../list/components/packages_search_spec.js | 145 +++++ .../packages/list/components/packages_sort_spec.js | 90 --- .../components/tokens/package_type_token_spec.js | 48 ++ spec/frontend/packages/list/stores/actions_spec.js | 12 +- .../packages/list/stores/mutations_spec.js | 9 +- .../__snapshots__/package_list_row_spec.js.snap | 10 +- .../shared/components/packages_list_loader_spec.js | 4 +- .../group/components/group_settings_app_spec.js | 99 +++ .../settings/group/mock_data.js | 12 + .../projects/edit/mount_search_settings_spec.js | 25 - .../pages/projects/graphs/code_coverage_spec.js | 2 +- .../permissions/components/settings_panel_spec.js | 8 +- .../components/commit/commit_form_spec.js | 6 +- .../components/commit/commit_section_spec.js | 223 +++++++ .../header/pipeline_editor_header_spec.js | 34 + .../components/header/validation_segment_spec.js | 120 ++++ .../components/info/validation_segment_spec.js | 113 ---- .../components/pipeline_editor_tabs_spec.js | 129 ++++ .../pipeline_editor/components/text_editor_spec.js | 93 ++- .../ui/confirm_unsaved_changes_dialog_spec.js | 42 ++ .../pipeline_editor/graphql/resolvers_spec.js | 6 +- .../pipeline_editor/pipeline_editor_app_spec.js | 424 +++---------- .../pipeline_editor/pipeline_editor_home_spec.js | 50 ++ .../components/pipeline_new_form_spec.js | 113 ++-- spec/frontend/pipelines/blank_state_spec.js | 21 +- .../pipelines/graph/graph_component_legacy_spec.js | 2 +- .../pipelines/graph/graph_component_spec.js | 12 +- .../pipelines/graph/linked_pipeline_spec.js | 7 +- .../pipelines/graph/stage_column_component_spec.js | 4 + .../__snapshots__/links_inner_spec.js.snap | 23 + .../pipelines/graph_shared/links_inner_spec.js | 197 ++++++ .../pipelines/graph_shared/links_layer_spec.js | 99 +++ spec/frontend/pipelines/header_component_spec.js | 8 +- .../pipelines/legacy_header_component_spec.js | 116 ---- .../frontend/pipelines/pipeline_graph/mock_data.js | 135 +++- .../pipeline_graph/pipeline_graph_spec.js | 2 +- spec/frontend/pipelines/pipeline_url_spec.js | 28 +- spec/frontend/pipelines/pipelines_actions_spec.js | 64 +- spec/frontend/pipelines/pipelines_spec.js | 6 +- .../frontend/pipelines/pipelines_table_row_spec.js | 4 +- spec/frontend/pipelines/shared/links_layer_spec.js | 99 --- spec/frontend/pipelines/stage_spec.js | 278 +++++++-- .../pipelines/test_reports/stores/actions_spec.js | 5 +- .../test_reports/test_case_details_spec.js | 35 ++ .../test_reports/test_suite_table_spec.js | 2 +- .../tokens/pipeline_trigger_author_token_spec.js | 2 +- .../components/delete_account_modal_spec.js | 2 +- .../commit/components/branches_dropdown_spec.js | 2 +- .../projects/commit/components/form_modal_spec.js | 5 +- .../frontend/projects/commit/store/actions_spec.js | 2 +- .../projects/commit_box/info/load_branches_spec.js | 2 +- .../projects/compare/components/app_spec.js | 116 ++++ .../compare/components/revision_dropdown_spec.js | 92 +++ .../project_delete_button_spec.js.snap | 4 +- spec/frontend/projects/members/utils_spec.js | 14 + .../pipelines/charts/components/app_spec.js | 118 ++-- .../charts/components/pipeline_charts_spec.js | 51 +- .../components/service_desk_root_spec.js | 236 ++++--- .../components/service_desk_setting_spec.js | 190 +++--- .../services/service_desk_service_spec.js | 111 ---- .../explorer/components/delete_image_spec.js | 153 +++++ .../components/details_page/empty_state_spec.js | 54 ++ .../details_page/empty_tags_state_spec.js | 43 -- .../components/list_page/group_empty_state_spec.js | 2 +- .../list_page/project_empty_state_spec.js | 2 +- spec/frontend/registry/explorer/mock_data.js | 6 + .../registry/explorer/pages/details_spec.js | 58 +- spec/frontend/registry/explorer/pages/list_spec.js | 49 +- .../frontend/releases/components/app_index_spec.js | 2 +- .../components/release_block_footer_spec.js | 2 +- .../releases/stores/modules/detail/actions_spec.js | 2 +- .../releases/stores/modules/list/actions_spec.js | 2 +- .../releases/stores/modules/list/mutations_spec.js | 2 +- .../grouped_codequality_reports_app_spec.js | 5 +- .../reports/codequality_report/mock_data.js | 50 ++ .../codequality_report/store/actions_spec.js | 169 +++-- .../codequality_report/store/mutations_spec.js | 14 + .../store/utils/codequality_comparison_spec.js | 18 +- .../components/grouped_test_reports_app_spec.js | 3 +- .../reports/components/summary_row_spec.js | 4 +- .../repository/components/last_commit_spec.js | 96 ++- .../search/highlight_blob_search_result_spec.js | 3 +- spec/frontend/search/index_spec.js | 3 + spec/frontend/search/mock_data.js | 22 +- spec/frontend/search/sort/components/app_spec.js | 168 +++++ spec/frontend/search/topbar/components/app_spec.js | 113 ++++ .../topbar/components/project_filter_spec.js | 2 +- spec/frontend/search_settings/index_spec.js | 37 +- spec/frontend/search_settings/mount_spec.js | 36 ++ spec/frontend/search_spec.js | 23 - .../serverless/components/environment_row_spec.js | 2 +- spec/frontend/serverless/store/actions_spec.js | 2 +- .../sidebar/__snapshots__/todo_spec.js.snap | 2 +- spec/frontend/sidebar/assignees_realtime_spec.js | 2 +- spec/frontend/sidebar/assignees_spec.js | 2 +- .../components/time_tracking/time_tracker_spec.js | 2 +- spec/frontend/sidebar/reviewers_spec.js | 9 +- spec/frontend/sidebar/subscriptions_spec.js | 25 +- spec/frontend/sidebar/todo_spec.js | 4 +- spec/frontend/snippets/components/show_spec.js | 2 +- .../components/edit_meta_modal_spec.js | 2 +- .../static_site_editor/pages/success_spec.js | 2 +- .../services/front_matterify_spec.js | 3 +- .../services/parse_source_file_spec.js | 3 +- .../components/states_table_actions_spec.js | 89 ++- .../terraform/components/states_table_spec.js | 19 + .../terraform/components/terraform_list_spec.js | 25 +- .../user_lists/components/edit_user_list_spec.js | 2 +- .../mr_widget_pipeline_container_spec.js | 2 +- .../components/mr_widget_status_icon_spec.js | 46 +- .../components/mr_widget_suggest_pipeline_spec.js | 4 +- .../mr_widget_pipeline_failed_spec.js.snap | 24 + .../states/mr_widget_auto_merge_enabled_spec.js | 6 +- .../states/mr_widget_failed_to_merge_spec.js | 10 +- .../components/states/mr_widget_merged_spec.js | 16 + .../states/mr_widget_pipeline_blocked_spec.js | 26 +- .../states/mr_widget_pipeline_failed_spec.js | 39 +- .../mr_widget_terraform_container_spec.js | 2 +- .../components/terraform/terraform_plan_spec.js | 8 +- .../vue_mr_widget/mr_widget_options_spec.js | 75 +-- .../vue_shared/alert_details/alert_details_spec.js | 351 +++++++++++ .../alert_management_sidebar_todo_spec.js | 112 ++++ .../vue_shared/alert_details/alert_metrics_spec.js | 62 ++ .../vue_shared/alert_details/alert_status_spec.js | 166 +++++ .../alert_details/alert_summary_row_spec.js | 40 ++ .../vue_shared/alert_details/mocks/alerts.json | 71 +++ .../alert_managment_sidebar_assignees_spec.js | 173 ++++++ .../alert_details/sidebar/alert_sidebar_spec.js | 64 ++ .../sidebar/alert_sidebar_status_spec.js | 103 ++++ .../alert_management_system_note_spec.js | 40 ++ .../components/color_picker/color_picker_spec.js | 39 +- .../vue_shared/components/editor_lite_spec.js | 9 +- .../vue_shared/components/file_row_spec.js | 2 +- .../__snapshots__/utils_spec.js.snap | 8 +- .../vue_shared/components/gl_countdown_spec.js | 2 +- .../vue_shared/components/gl_modal_vuex_spec.js | 5 +- .../vue_shared/components/help_popover_spec.js | 65 ++ .../components/issue/issue_milestone_spec.js | 2 +- .../vue_shared/components/markdown/field_spec.js | 2 +- .../components/modal_copy_button_spec.js | 3 +- .../rich_content_editor/toolbar_item_spec.js | 2 +- .../components/runner_instructions/mock_data.js | 107 ++++ .../runner_instructions_spec.js | 113 ++++ .../__snapshots__/settings_block_spec.js.snap | 43 ++ .../components/settings/settings_block_spec.js | 86 +++ .../vue_shared/components/todo_button_spec.js | 2 +- .../user_avatar/user_avatar_link_spec.js | 2 +- .../vue_shared/directives/track_event_spec.js | 2 +- .../vue_shared/security_reports/mock_data.js | 17 + .../security_reports/security_reports_app_spec.js | 368 +++-------- spec/frontend/whats_new/store/actions_spec.js | 2 +- spec/frontend_integration/ide/helpers/start.js | 2 +- .../ide/user_opens_file_spec.js | 2 +- .../ide/user_opens_ide_spec.js | 2 +- spec/frontend_integration/test_helpers/fixtures.js | 8 +- .../mutations/can_mutate_spammable_spec.rb | 46 ++ .../ci_configuration/configure_sast_spec.rb | 120 ++++ spec/graphql/resolvers/base_resolver_spec.rb | 4 +- .../resolvers/board_list_issues_resolver_spec.rb | 6 +- .../graphql/resolvers/board_lists_resolver_spec.rb | 12 +- spec/graphql/resolvers/issues_resolver_spec.rb | 12 +- .../resolvers/merge_requests_resolver_spec.rb | 62 +- .../resolvers/package_details_resolver_spec.rb | 5 +- .../resolvers/release_milestones_resolver_spec.rb | 6 +- .../resolvers/terraform/states_resolver_spec.rb | 20 +- .../sast/analyzers_entity_input_type_spec.rb | 9 + .../sast/analyzers_entity_type_spec.rb | 11 + .../sast/entity_input_type_spec.rb | 9 + .../ci_configuration/sast/entity_type_spec.rb | 11 + .../types/ci_configuration/sast/input_type_spec.rb | 9 + .../ci_configuration/sast/options_entity_spec.rb | 11 + .../types/ci_configuration/sast/type_spec.rb | 11 + .../sast/ui_component_size_enum_spec.rb | 11 + .../types/packages/composer/details_type_spec.rb | 23 - .../types/packages/composer/metadatum_type_spec.rb | 4 +- spec/graphql/types/packages/package_type_spec.rb | 7 +- .../packages/package_without_versions_type_spec.rb | 13 + spec/graphql/types/project_type_spec.rb | 168 ++++- spec/graphql/types/query_type_spec.rb | 6 +- spec/helpers/commits_helper_spec.rb | 31 +- spec/helpers/container_registry_helper_spec.rb | 4 +- spec/helpers/diff_helper_spec.rb | 24 +- spec/helpers/enable_search_settings_helper_spec.rb | 21 + spec/helpers/groups/group_members_helper_spec.rb | 16 +- spec/helpers/groups_helper_spec.rb | 78 +++ spec/helpers/invite_members_helper_spec.rb | 87 ++- spec/helpers/issuables_helper_spec.rb | 46 +- spec/helpers/issues_helper_spec.rb | 27 + spec/helpers/jira_connect_helper_spec.rb | 41 +- spec/helpers/notes_helper_spec.rb | 11 + spec/helpers/operations_helper_spec.rb | 2 +- .../projects/project_members_helper_spec.rb | 56 ++ spec/helpers/projects_helper_spec.rb | 98 ++- spec/helpers/search_helper_spec.rb | 21 + spec/helpers/sorting_helper_spec.rb | 18 - spec/helpers/stat_anchors_helper_spec.rb | 4 +- spec/helpers/tree_helper_spec.rb | 88 --- spec/helpers/user_callouts_helper_spec.rb | 20 + spec/initializers/validate_puma_spec.rb | 92 +++ spec/javascripts/test_bundle.js | 8 - spec/lib/api/entities/user_spec.rb | 12 + spec/lib/atlassian/jira_connect/client_spec.rb | 66 +- .../serializers/feature_flag_entity_spec.rb | 5 +- spec/lib/backup/files_spec.rb | 10 +- spec/lib/banzai/filter/asset_proxy_filter_spec.rb | 16 +- .../banzai/filter/truncate_source_filter_spec.rb | 2 +- spec/lib/banzai/pipeline/full_pipeline_spec.rb | 12 + .../pipeline/plain_markdown_pipeline_spec.rb | 118 ++++ .../banzai/pipeline/pre_process_pipeline_spec.rb | 2 +- .../common/extractors/graphql_extractor_spec.rb | 66 +- .../common/loaders/entity_loader_spec.rb | 2 +- .../common/transformers/hash_key_digger_spec.rb | 28 - .../underscorify_keys_transformer_spec.rb | 27 - .../groups/extractors/subgroups_extractor_spec.rb | 24 + .../groups/graphql/get_group_query_spec.rb | 31 + .../groups/graphql/get_labels_query_spec.rb | 31 + .../groups/loaders/group_loader_spec.rb | 17 +- .../groups/loaders/labels_loader_spec.rb | 30 + .../groups/pipelines/group_pipeline_spec.rb | 49 +- .../groups/pipelines/labels_pipeline_spec.rb | 116 ++++ .../pipelines/subgroup_entities_pipeline_spec.rb | 8 +- .../group_attributes_transformer_spec.rb | 24 +- .../bulk_imports/importers/group_importer_spec.rb | 13 +- spec/lib/bulk_imports/pipeline/context_spec.rb | 38 +- .../bulk_imports/pipeline/extracted_data_spec.rb | 53 ++ spec/lib/bulk_imports/pipeline/runner_spec.rb | 76 ++- spec/lib/gitlab/access/branch_protection_spec.rb | 10 +- spec/lib/gitlab/auth/ip_rate_limiter_spec.rb | 3 + .../lib/gitlab/auth/u2f_webauthn_converter_spec.rb | 29 + ...move_duplicate_vulnerabilities_findings_spec.rb | 135 ++++ spec/lib/gitlab/background_migration_spec.rb | 2 +- spec/lib/gitlab/badge/coverage/metadata_spec.rb | 32 - spec/lib/gitlab/badge/coverage/report_spec.rb | 99 --- spec/lib/gitlab/badge/coverage/template_spec.rb | 182 ------ spec/lib/gitlab/badge/pipeline/metadata_spec.rb | 29 - spec/lib/gitlab/badge/pipeline/status_spec.rb | 127 ---- spec/lib/gitlab/badge/pipeline/template_spec.rb | 140 ----- spec/lib/gitlab/badge/shared/metadata.rb | 33 - spec/lib/gitlab/changelog/committer_spec.rb | 128 ++++ spec/lib/gitlab/changelog/config_spec.rb | 96 +++ spec/lib/gitlab/changelog/generator_spec.rb | 164 +++++ spec/lib/gitlab/changelog/release_spec.rb | 107 ++++ .../lib/gitlab/changelog/template/compiler_spec.rb | 136 ++++ spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb | 32 + spec/lib/gitlab/ci/badge/coverage/report_spec.rb | 99 +++ spec/lib/gitlab/ci/badge/coverage/template_spec.rb | 182 ++++++ spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb | 29 + spec/lib/gitlab/ci/badge/pipeline/status_spec.rb | 127 ++++ spec/lib/gitlab/ci/badge/pipeline/template_spec.rb | 140 +++++ spec/lib/gitlab/ci/badge/shared/metadata.rb | 33 + .../credentials/registry/dependency_proxy_spec.rb | 43 ++ .../credentials/registry/gitlab_registry_spec.rb | 43 ++ .../gitlab/ci/build/credentials/registry_spec.rb | 43 -- spec/lib/gitlab/ci/build/rules_spec.rb | 29 +- spec/lib/gitlab/ci/charts_spec.rb | 82 +++ spec/lib/gitlab/ci/config/entry/cache_spec.rb | 10 +- spec/lib/gitlab/ci/config/entry/job_spec.rb | 10 - .../lib/gitlab/ci/config/entry/processable_spec.rb | 29 + spec/lib/gitlab/ci/config/external/mapper_spec.rb | 14 - spec/lib/gitlab/ci/cron_parser_spec.rb | 11 + spec/lib/gitlab/ci/parsers/instrumentation_spec.rb | 27 + spec/lib/gitlab/ci/parsers_spec.rb | 8 + .../chain/cancel_pending_pipelines_spec.rb | 14 - .../gitlab/ci/reports/codequality_mr_diff_spec.rb | 58 ++ .../reports/codequality_reports_comparer_spec.rb | 58 +- .../gitlab/ci/reports/codequality_reports_spec.rb | 58 +- spec/lib/gitlab/ci/trace/chunked_io_spec.rb | 2 +- spec/lib/gitlab/ci/variables/helpers_spec.rb | 103 ++++ .../cleanup/orphan_job_artifact_files_spec.rb | 3 +- spec/lib/gitlab/cluster/lifecycle_events_spec.rb | 85 +++ spec/lib/gitlab/composer/cache_spec.rb | 133 ++++ spec/lib/gitlab/composer/version_index_spec.rb | 16 +- spec/lib/gitlab/conan_token_spec.rb | 2 +- spec/lib/gitlab/crypto_helper_spec.rb | 78 ++- spec/lib/gitlab/current_settings_spec.rb | 28 + spec/lib/gitlab/danger/base_linter_spec.rb | 193 ------ spec/lib/gitlab/danger/changelog_spec.rb | 229 ------- spec/lib/gitlab/danger/commit_linter_spec.rb | 242 -------- spec/lib/gitlab/danger/danger_spec_helper.rb | 17 - spec/lib/gitlab/danger/emoji_checker_spec.rb | 38 -- spec/lib/gitlab/danger/helper_spec.rb | 602 ------------------ .../lib/gitlab/danger/merge_request_linter_spec.rb | 55 -- spec/lib/gitlab/danger/roulette_spec.rb | 413 ------------- spec/lib/gitlab/danger/sidekiq_queues_spec.rb | 82 --- spec/lib/gitlab/danger/teammate_spec.rb | 220 ------- spec/lib/gitlab/danger/title_linting_spec.rb | 56 -- .../gitlab/danger/weightage/maintainers_spec.rb | 34 - spec/lib/gitlab/danger/weightage/reviewers_spec.rb | 63 -- spec/lib/gitlab/data_builder/build_spec.rb | 4 +- spec/lib/gitlab/data_builder/pipeline_spec.rb | 4 +- .../gitlab/database/migration_helpers/v2_spec.rb | 221 +++++++ spec/lib/gitlab/database/migration_helpers_spec.rb | 253 -------- .../table_management_helpers_spec.rb | 3 +- spec/lib/gitlab/database/with_lock_retries_spec.rb | 4 + spec/lib/gitlab/diff/char_diff_spec.rb | 77 +++ .../lib/gitlab/diff/file_collection_sorter_spec.rb | 10 +- spec/lib/gitlab/diff/highlight_cache_spec.rb | 18 + spec/lib/gitlab/diff/inline_diff_spec.rb | 27 + .../experimentation/controller_concern_spec.rb | 27 + spec/lib/gitlab/experimentation/experiment_spec.rb | 3 +- spec/lib/gitlab/experimentation_spec.rb | 65 +- spec/lib/gitlab/file_finder_spec.rb | 8 + spec/lib/gitlab/git/commit_spec.rb | 3 +- spec/lib/gitlab/git/diff_spec.rb | 7 + spec/lib/gitlab/git/repository_spec.rb | 5 +- .../gitlab/graphql/pagination/connections_spec.rb | 97 +++ spec/lib/gitlab/hook_data/group_builder_spec.rb | 68 ++ spec/lib/gitlab/hook_data/subgroup_builder_spec.rb | 52 ++ spec/lib/gitlab/import_export/all_models.yml | 5 +- .../import_export/design_repo_restorer_spec.rb | 4 +- .../gitlab/import_export/design_repo_saver_spec.rb | 2 +- spec/lib/gitlab/import_export/fork_spec.rb | 4 +- .../import_export/group/tree_restorer_spec.rb | 1 + spec/lib/gitlab/import_export/importer_spec.rb | 4 +- .../lib/gitlab/import_export/repo_restorer_spec.rb | 10 +- spec/lib/gitlab/import_export/repo_saver_spec.rb | 10 +- .../gitlab/import_export/safe_model_attributes.yml | 4 + spec/lib/gitlab/import_export/saver_spec.rb | 12 +- .../gitlab/import_export/wiki_repo_saver_spec.rb | 2 +- .../redis_cluster_validator_spec.rb | 1 + spec/lib/gitlab/instrumentation_helper_spec.rb | 6 +- spec/lib/gitlab/kas_spec.rb | 44 ++ .../metrics/subscribers/external_http_spec.rb | 172 ++++++ .../gitlab/metrics/subscribers/rack_attack_spec.rb | 203 ++++++ spec/lib/gitlab/patch/prependable_spec.rb | 18 + spec/lib/gitlab/performance_bar/stats_spec.rb | 8 +- .../rack_attack/instrumented_cache_store_spec.rb | 89 +++ spec/lib/gitlab/rack_attack_spec.rb | 3 + spec/lib/gitlab/repository_cache_adapter_spec.rb | 7 +- spec/lib/gitlab/search/query_spec.rb | 18 + spec/lib/gitlab/search_results_spec.rb | 7 +- .../sidekiq_logging/structured_logger_spec.rb | 3 +- spec/lib/gitlab/suggestions/commit_message_spec.rb | 11 + .../finders/global_template_finder_spec.rb | 23 +- .../terraform/state_migration_helper_spec.rb | 21 + spec/lib/gitlab/tracking/standard_context_spec.rb | 50 +- spec/lib/gitlab/url_blocker_spec.rb | 46 +- spec/lib/gitlab/url_blockers/url_allowlist_spec.rb | 28 +- spec/lib/gitlab/usage/docs/renderer_spec.rb | 21 + spec/lib/gitlab/usage/docs/value_formatter_spec.rb | 26 + spec/lib/gitlab/usage/metric_definition_spec.rb | 23 +- spec/lib/gitlab/usage/metric_spec.rb | 8 +- .../usage/metrics/aggregates/aggregate_spec.rb | 256 ++++++++ .../usage_data_counters/aggregated_metrics_spec.rb | 4 +- .../usage_data_counters/hll_redis_counter_spec.rb | 195 ++---- .../merge_request_activity_unique_counter_spec.rb | 48 ++ .../quick_action_activity_unique_counter_spec.rb | 163 +++++ spec/lib/gitlab/usage_data_spec.rb | 144 +++-- spec/lib/gitlab/utils/markdown_spec.rb | 44 +- spec/lib/gitlab/utils/override_spec.rb | 67 ++ spec/lib/gitlab/utils/usage_data_spec.rb | 103 ++++ spec/lib/gitlab/utils_spec.rb | 8 - spec/lib/gitlab_danger_spec.rb | 76 --- spec/lib/gitlab_spec.rb | 24 +- spec/lib/peek/views/external_http_spec.rb | 191 ++++++ .../lib/release_highlights/validator/entry_spec.rb | 2 +- spec/lib/release_highlights/validator_spec.rb | 2 +- .../ci_configuration/sast_build_actions_spec.rb | 539 ++++++++++++++++ spec/mailers/notify_spec.rb | 13 + ...move_duplicate_vulnerabilities_findings_spec.rb | 140 +++++ ...a_issue_first_mentioned_in_commit_value_spec.rb | 30 + .../add_has_external_issue_tracker_trigger_spec.rb | 164 +++++ .../encrypt_feature_flags_clients_tokens_spec.rb | 2 +- .../schedule_migrate_security_scans_spec.rb | 11 +- spec/models/active_session_spec.rb | 2 +- spec/models/application_setting_spec.rb | 18 +- spec/models/bulk_imports/entity_spec.rb | 31 + spec/models/ci/build_dependencies_spec.rb | 28 + spec/models/ci/build_spec.rb | 66 +- spec/models/ci/build_trace_chunk_spec.rb | 2 +- spec/models/ci/build_trace_chunks/fog_spec.rb | 21 - spec/models/ci/pipeline_artifact_spec.rb | 100 ++- spec/models/ci/pipeline_spec.rb | 107 +++- spec/models/ci/processable_spec.rb | 54 ++ spec/models/ci/resource_group_spec.rb | 12 +- spec/models/ci/resource_spec.rb | 2 +- spec/models/ci/stage_spec.rb | 3 +- spec/models/commit_spec.rb | 13 + spec/models/commit_status_spec.rb | 17 - spec/models/concerns/atomic_internal_id_spec.rb | 152 +++++ spec/models/concerns/bulk_insert_safe_spec.rb | 7 +- spec/models/concerns/featurable_spec.rb | 16 - spec/models/concerns/issuable_spec.rb | 19 +- spec/models/concerns/spammable_spec.rb | 55 ++ spec/models/concerns/token_authenticatable_spec.rb | 4 +- .../encrypted_spec.rb | 8 +- spec/models/dependency_proxy/manifest_spec.rb | 20 +- spec/models/deployment_spec.rb | 20 - spec/models/experiment_spec.rb | 57 ++ spec/models/group_spec.rb | 24 +- spec/models/issue_link_spec.rb | 2 +- spec/models/merge_request/metrics_spec.rb | 11 + spec/models/merge_request_diff_commit_spec.rb | 9 +- spec/models/merge_request_diff_spec.rb | 45 +- spec/models/merge_request_spec.rb | 143 ++++- spec/models/onboarding_progress_spec.rb | 67 ++ spec/models/packages/composer/cache_file_spec.rb | 32 + spec/models/packages/composer/metadatum_spec.rb | 16 + .../models/packages/debian/group_component_spec.rb | 7 + .../packages/debian/project_component_spec.rb | 7 + spec/models/pages/lookup_path_spec.rb | 47 ++ spec/models/pages/virtual_domain_spec.rb | 21 +- .../chat_notification_service_spec.rb | 33 + .../project_services/confluence_service_spec.rb | 6 +- .../project_services/datadog_service_spec.rb | 36 +- spec/models/project_spec.rb | 174 +++--- spec/models/project_wiki_spec.rb | 1 + spec/models/prometheus_metric_spec.rb | 10 +- spec/models/readme_blob_spec.rb | 17 - spec/models/release_spec.rb | 57 +- spec/models/repository_spec.rb | 74 ++- spec/models/service_spec.rb | 78 +-- spec/models/snippet_spec.rb | 6 +- spec/models/terraform/state_spec.rb | 12 + spec/models/terraform/state_version_spec.rb | 18 + spec/models/token_with_iv_spec.rb | 29 + spec/models/u2f_registration_spec.rb | 37 ++ spec/models/user_spec.rb | 2 +- spec/policies/project_policy_spec.rb | 65 ++ spec/presenters/ci/build_runner_presenter_spec.rb | 10 - .../code_quality_mr_diff_presenter_spec.rb | 66 ++ .../gitlab/whats_new/item_presenter_spec.rb | 29 - spec/presenters/project_presenter_spec.rb | 2 +- spec/requests/api/api_spec.rb | 36 ++ spec/requests/api/applications_spec.rb | 2 +- spec/requests/api/debian_group_packages_spec.rb | 20 +- spec/requests/api/debian_project_packages_spec.rb | 28 +- spec/requests/api/deploy_tokens_spec.rb | 16 - spec/requests/api/events_spec.rb | 6 + spec/requests/api/generic_packages_spec.rb | 11 +- spec/requests/api/graphql/issue/issue_spec.rb | 14 + .../http_integration/update_spec.rb | 16 +- .../merge_requests/reviewer_rereview_spec.rb | 65 ++ .../api/graphql/mutations/releases/create_spec.rb | 8 +- .../api/graphql/mutations/snippets/create_spec.rb | 36 +- .../api/graphql/mutations/snippets/update_spec.rb | 34 +- .../packages/package_composer_details_spec.rb | 39 -- spec/requests/api/graphql/packages/package_spec.rb | 80 +++ .../graphql/project/issue/designs/notes_spec.rb | 19 +- .../api/graphql/project/merge_requests_spec.rb | 94 ++- spec/requests/api/graphql/project/packages_spec.rb | 31 +- spec/requests/api/graphql/project/release_spec.rb | 4 +- .../api/graphql/project/terraform/state_spec.rb | 83 +++ spec/requests/api/group_labels_spec.rb | 54 +- spec/requests/api/groups_spec.rb | 7 +- spec/requests/api/internal/base_spec.rb | 98 +++ spec/requests/api/internal/kubernetes_spec.rb | 68 +- spec/requests/api/labels_spec.rb | 59 +- spec/requests/api/maven_packages_spec.rb | 11 +- spec/requests/api/merge_requests_spec.rb | 62 +- spec/requests/api/oauth_tokens_spec.rb | 15 +- spec/requests/api/projects_spec.rb | 35 +- spec/requests/api/pypi_packages_spec.rb | 9 +- spec/requests/api/repositories_spec.rb | 98 +++ spec/requests/api/resource_access_tokens_spec.rb | 293 +++++++++ spec/requests/api/settings_spec.rb | 18 +- spec/requests/api/suggestions_spec.rb | 27 +- spec/requests/api/templates_spec.rb | 6 +- spec/requests/api/users_spec.rb | 8 +- spec/requests/api/version_spec.rb | 12 + .../groups/email_campaigns_controller_spec.rb | 86 +++ spec/requests/oauth/tokens_controller_spec.rb | 14 + .../histograms_controller_spec.rb | 46 ++ spec/requests/projects/noteable_notes_spec.rb | 11 +- spec/requests/rack_attack_global_spec.rb | 6 +- spec/requests/users_controller_spec.rb | 8 + spec/requests/whats_new_controller_spec.rb | 60 +- spec/routing/admin_routing_spec.rb | 7 - .../configuration_controller_routing_spec.rb | 15 + spec/rubocop/code_reuse_helpers_spec.rb | 56 +- .../cop/active_record_association_reload_spec.rb | 2 - spec/rubocop/cop/api/base_spec.rb | 5 +- .../cop/api/grape_array_missing_coerce_spec.rb | 28 +- spec/rubocop/cop/avoid_becomes_spec.rb | 30 +- .../cop/avoid_break_from_strong_memoize_spec.rb | 6 +- ...id_keyword_arguments_in_sidekiq_workers_spec.rb | 7 +- spec/rubocop/cop/ban_catch_throw_spec.rb | 26 +- spec/rubocop/cop/code_reuse/finder_spec.rb | 5 - spec/rubocop/cop/code_reuse/presenter_spec.rb | 3 - spec/rubocop/cop/code_reuse/serializer_spec.rb | 3 - spec/rubocop/cop/code_reuse/service_class_spec.rb | 3 - spec/rubocop/cop/code_reuse/worker_spec.rb | 3 - spec/rubocop/cop/default_scope_spec.rb | 39 +- .../gitlab/avoid_uploaded_file_from_params_spec.rb | 9 +- spec/rubocop/cop/gitlab/bulk_insert_spec.rb | 11 +- spec/rubocop/cop/gitlab/change_timezone_spec.rb | 5 +- .../cop/gitlab/const_get_inherit_false_spec.rb | 47 +- .../cop/gitlab/duplicate_spec_location_spec.rb | 2 - spec/rubocop/cop/gitlab/except_spec.rb | 3 - .../rubocop/cop/gitlab/finder_with_find_by_spec.rb | 49 +- spec/rubocop/cop/gitlab/httparty_spec.rb | 34 +- spec/rubocop/cop/gitlab/intersect_spec.rb | 3 - spec/rubocop/cop/gitlab/json_spec.rb | 33 +- .../gitlab/module_with_instance_variables_spec.rb | 35 +- spec/rubocop/cop/gitlab/namespaced_class_spec.rb | 73 +++ .../rubocop/cop/gitlab/policy_rule_boolean_spec.rb | 3 - .../cop/gitlab/predicate_memoization_spec.rb | 74 +-- spec/rubocop/cop/gitlab/rails_logger_spec.rb | 22 +- spec/rubocop/cop/gitlab/union_spec.rb | 3 - spec/rubocop/cop/graphql/authorize_types_spec.rb | 7 +- spec/rubocop/cop/graphql/descriptions_spec.rb | 22 +- spec/rubocop/cop/graphql/gid_expected_type_spec.rb | 7 +- spec/rubocop/cop/graphql/id_type_spec.rb | 7 +- spec/rubocop/cop/graphql/json_type_spec.rb | 24 +- spec/rubocop/cop/graphql/resolver_type_spec.rb | 18 +- .../cop/group_public_or_visible_to_user_spec.rb | 23 +- .../cop/inject_enterprise_edition_module_spec.rb | 24 +- .../rubocop/cop/lint/last_keyword_argument_spec.rb | 2 - .../migration/add_limit_to_text_columns_spec.rb | 18 + .../cop/put_project_routes_under_scope_spec.rb | 2 - .../cop/qa/ambiguous_page_object_name_spec.rb | 4 - spec/rubocop/cop/qa/element_with_pattern_spec.rb | 4 - .../cop/rspec/factories_in_migration_specs_spec.rb | 2 +- .../cop/ruby_interpolation_in_translation_spec.rb | 67 +- .../cop/static_translation_definition_spec.rb | 98 ++- .../distinct_count_by_large_foreign_key_spec.rb | 42 +- spec/rubocop/cop/usage_data/large_table_spec.rb | 56 +- spec/rubocop/qa_helpers_spec.rb | 6 +- spec/serializers/admin/user_entity_spec.rb | 1 + spec/serializers/admin/user_serializer_spec.rb | 1 + .../ci/codequality_mr_diff_entity_spec.rb | 27 + .../codequality_mr_diff_report_serializer_spec.rb | 32 + spec/serializers/ci/pipeline_entity_spec.rb | 261 ++++++++ .../codequality_degradation_entity_spec.rb | 63 +- .../codequality_reports_comparer_entity_spec.rb | 58 +- ...codequality_reports_comparer_serializer_spec.rb | 58 +- spec/serializers/diff_file_metadata_entity_spec.rb | 27 + spec/serializers/diffs_entity_spec.rb | 15 +- spec/serializers/group_group_link_entity_spec.rb | 31 - .../group_group_link_serializer_spec.rb | 13 - .../group_link/group_group_link_entity_spec.rb | 31 + .../group_link/group_group_link_serializer_spec.rb | 13 + .../group_link/group_link_entity_spec.rb | 25 + .../group_link/project_group_link_entity_spec.rb | 30 + .../project_group_link_serializer_spec.rb | 13 + spec/serializers/member_entity_spec.rb | 36 +- spec/serializers/member_serializer_spec.rb | 4 +- spec/serializers/merge_request_user_entity_spec.rb | 18 + .../merge_request_widget_entity_spec.rb | 6 +- spec/serializers/pipeline_details_entity_spec.rb | 4 +- spec/serializers/pipeline_entity_spec.rb | 264 -------- spec/serializers/user_serializer_spec.rb | 2 +- .../process_prometheus_alert_service_spec.rb | 2 +- .../application_settings/update_service_spec.rb | 2 +- .../recalculate_for_user_range_service_spec.rb | 2 +- .../bulk_create_integration_service_spec.rb | 51 -- .../captcha/captcha_verification_service_spec.rb | 39 ++ .../parent_child_pipeline_spec.rb | 25 + .../ci/create_pipeline_service/rules_spec.rb | 14 - spec/services/ci/create_pipeline_service_spec.rb | 4 +- ...rate_codequality_mr_diff_report_service_spec.rb | 51 ++ ...ate_code_quality_mr_diff_report_service_spec.rb | 62 ++ spec/services/ci/process_build_service_spec.rb | 6 +- spec/services/ci/process_pipeline_service_spec.rb | 29 + .../observe_histograms_service_spec.rb | 86 +++ spec/services/ci/register_job_service_spec.rb | 6 +- .../find_or_create_manifest_service_spec.rb | 46 +- .../dependency_proxy/head_manifest_service_spec.rb | 9 +- .../dependency_proxy/pull_manifest_service_spec.rb | 6 +- spec/services/deployments/create_service_spec.rb | 21 + spec/services/discussions/resolve_service_spec.rb | 14 + .../services/discussions/unresolve_service_spec.rb | 32 + spec/services/feature_flags/create_service_spec.rb | 12 - spec/services/feature_flags/update_service_spec.rb | 12 - spec/services/git/branch_hooks_service_spec.rb | 83 ++- spec/services/git/wiki_push_service_spec.rb | 50 ++ .../groups/import_export/export_service_spec.rb | 14 +- .../groups/open_issues_count_service_spec.rb | 106 ++++ .../integrations/test/project_service_spec.rb | 96 ++- spec/services/issue_rebalancing_service_spec.rb | 108 ++-- spec/services/issues/close_service_spec.rb | 3 + spec/services/issues/create_service_spec.rb | 176 +----- spec/services/issues/update_service_spec.rb | 2 +- spec/services/members/update_service_spec.rb | 24 +- .../merge_requests/after_create_service_spec.rb | 13 +- spec/services/merge_requests/build_service_spec.rb | 2 + .../delete_non_latest_diffs_service_spec.rb | 2 + .../mark_reviewer_reviewed_service_spec.rb | 45 ++ .../mergeability_check_service_spec.rb | 28 +- .../push_options_handler_service_spec.rb | 45 +- .../reload_merge_head_diff_service_spec.rb | 61 ++ .../merge_requests/request_review_service_spec.rb | 69 +++ .../services/merge_requests/update_service_spec.rb | 25 +- .../in_product_marketing_emails_service_spec.rb | 159 +++++ .../notification_recipients/build_service_spec.rb | 24 + spec/services/notification_service_spec.rb | 40 ++ .../debian/destroy_distribution_service_spec.rb | 71 +++ .../debian/update_distribution_service_spec.rb | 144 +++++ .../maven/find_or_create_package_service_spec.rb | 7 + .../migrate_from_legacy_storage_service_spec.rb | 92 +++ ...obtain_lets_encrypt_certificate_service_spec.rb | 2 +- spec/services/post_receive_service_spec.rb | 14 +- spec/services/projects/cleanup_service_spec.rb | 2 +- spec/services/projects/create_service_spec.rb | 44 +- spec/services/projects/fork_service_spec.rb | 44 ++ .../prometheus/alerts/notify_service_spec.rb | 7 +- .../services/projects/update_pages_service_spec.rb | 9 + .../update_repository_storage_service_spec.rb | 13 +- .../quick_actions/interpret_service_spec.rb | 24 + .../repositories/changelog_service_spec.rb | 74 +++ .../resource_access_tokens/create_service_spec.rb | 10 +- .../resource_access_tokens/revoke_service_spec.rb | 8 + .../change_milestone_service_spec.rb | 4 +- .../ci_configuration/sast_create_service_spec.rb | 69 +++ .../ci_configuration/sast_parser_service_spec.rb | 76 +++ spec/services/snippets/create_service_spec.rb | 5 +- .../update_repository_storage_service_spec.rb | 13 +- spec/services/snippets/update_service_spec.rb | 13 +- spec/services/spam/spam_action_service_spec.rb | 182 ++++-- spec/services/suggestions/apply_service_spec.rb | 104 +++- spec/services/suggestions/create_service_spec.rb | 33 +- spec/services/system_hooks_service_spec.rb | 9 +- .../system_notes/issuables_service_spec.rb | 2 + spec/services/test_hooks/project_service_spec.rb | 93 ++- spec/services/todo_service_spec.rb | 11 + spec/services/users/approve_service_spec.rb | 24 +- spec/services/users/build_service_spec.rb | 6 +- .../refresh_authorized_projects_service_spec.rb | 15 + spec/services/users/reject_service_spec.rb | 20 + spec/spec_helper.rb | 13 +- spec/support/gitlab_experiment.rb | 12 + spec/support/graphql/field_selection.rb | 2 +- spec/support/helpers/dependency_proxy_helpers.rb | 4 +- spec/support/helpers/graphql_helpers.rb | 12 +- spec/support/helpers/next_found_instance_of.rb | 16 +- spec/support/helpers/smime_helper.rb | 2 +- spec/support/helpers/snowplow_helpers.rb | 10 +- spec/support/helpers/sorting_helper.rb | 31 + spec/support/helpers/stub_configuration.rb | 6 + spec/support/helpers/stub_object_storage.rb | 7 + spec/support/helpers/wait_for_requests.rb | 2 +- .../pushed_frontend_feature_flags_matcher.rb | 23 + .../track_self_describing_event_matcher.rb | 12 + .../shared_contexts/navbar_structure_context.rb | 7 +- .../requests/api/conan_packages_shared_context.rb | 2 +- .../multiple_issue_boards_shared_examples.rb | 2 + .../comment_and_close_button_shared_examples.rb | 29 + .../features/discussion_comments_shared_example.rb | 3 +- .../editable_merge_request_shared_examples.rb | 2 +- .../multiple_reviewers_mr_shared_examples.rb | 2 +- .../mutations/can_mutate_spammable_examples.rb | 36 ++ .../mutations/http_integrations_shared_examples.rb | 75 +++ .../spammable_mutation_fields_examples.rb | 50 -- .../models/atomic_internal_id_shared_examples.rb | 95 ++- .../can_housekeep_repository_shared_examples.rb | 45 -- .../can_move_repository_storage_shared_examples.rb | 11 +- .../concerns/has_repository_shared_examples.rb | 32 - .../can_housekeep_repository_shared_examples.rb | 6 + .../repository_storage_movable_shared_examples.rb | 6 +- .../packages/debian/component_shared_examples.rb | 50 ++ .../debian/distribution_shared_examples.rb | 1 + .../api/debian_packages_shared_examples.rb | 34 +- .../api/graphql/noteable_shared_examples.rb | 62 ++ .../requests/api/read_user_shared_examples.rb | 32 +- .../repository_storage_moves_shared_examples.rb | 44 +- .../api/resolvable_discussions_shared_examples.rb | 3 + .../requests/rack_attack_shared_examples.rb | 8 +- ...e_repository_storage_service_shared_examples.rb | 19 +- .../repositories/housekeeping_shared_examples.rb | 18 +- .../change_milestone_service_shared_examples.rb | 8 +- .../services/snippets_shared_examples.rb | 68 +- .../git_garbage_collect_methods_shared_examples.rb | 320 ++++++++++ spec/support/snowplow.rb | 1 + spec/tasks/gitlab/cleanup_rake_spec.rb | 22 +- spec/tasks/gitlab/pages_rake_spec.rb | 60 +- spec/tasks/gitlab/password_rake_spec.rb | 76 +++ spec/tasks/gitlab/terraform/migrate_rake_spec.rb | 45 ++ spec/tooling/danger/base_linter_spec.rb | 192 ++++++ spec/tooling/danger/changelog_spec.rb | 228 +++++++ spec/tooling/danger/commit_linter_spec.rb | 241 ++++++++ spec/tooling/danger/danger_spec_helper.rb | 17 + spec/tooling/danger/emoji_checker_spec.rb | 37 ++ spec/tooling/danger/feature_flag_spec.rb | 157 +++++ spec/tooling/danger/helper_spec.rb | 682 +++++++++++++++++++++ spec/tooling/danger/merge_request_linter_spec.rb | 54 ++ spec/tooling/danger/roulette_spec.rb | 429 +++++++++++++ spec/tooling/danger/sidekiq_queues_spec.rb | 81 +++ spec/tooling/danger/teammate_spec.rb | 225 +++++++ spec/tooling/danger/title_linting_spec.rb | 91 +++ spec/tooling/danger/weightage/maintainers_spec.rb | 34 + spec/tooling/danger/weightage/reviewers_spec.rb | 63 ++ spec/tooling/gitlab_danger_spec.rb | 76 +++ .../packages/composer/cache_uploader_spec.rb | 45 ++ .../nested_attributes_duplicates_validator_spec.rb | 113 ++++ .../variable_duplicates_validator_spec.rb | 69 --- spec/views/groups/show.html.haml_spec.rb | 52 ++ spec/views/layouts/_head.html.haml_spec.rb | 38 ++ .../layouts/header/_new_dropdown.haml_spec.rb | 6 +- .../layouts/nav/sidebar/_project.html.haml_spec.rb | 2 +- .../_project_security_link.html.haml_spec.rb | 29 + spec/views/projects/_home_panel.html.haml_spec.rb | 1 + spec/views/projects/empty.html.haml_spec.rb | 37 ++ spec/views/projects/show.html.haml_spec.rb | 51 ++ .../projects/tree/_tree_row.html.haml_spec.rb | 43 -- spec/views/search/_filter.html.haml_spec.rb | 17 - spec/views/search/_form.html.haml_spec.rb | 14 - .../shared/ssh_keys/_key_details.html.haml_spec.rb | 20 + spec/workers/bulk_import_worker_spec.rb | 15 + spec/workers/bulk_imports/entity_worker_spec.rb | 14 + .../create_quality_report_worker_spec.rb | 40 ++ .../expire_artifacts_worker_spec.rb | 2 +- .../cleanup_container_repository_worker_spec.rb | 5 +- spec/workers/git_garbage_collect_worker_spec.rb | 346 +---------- .../jira_connect/sync_builds_worker_spec.rb | 24 - .../jira_connect/sync_deployments_worker_spec.rb | 24 - .../jira_connect/sync_feature_flags_worker_spec.rb | 24 - .../merge_request_cleanup_refs_worker_spec.rb | 12 + .../in_product_marketing_emails_worker_spec.rb | 29 + .../onboarding_pipeline_created_worker_spec.rb | 25 +- .../onboarding_user_added_worker_spec.rb | 17 +- .../projects/git_garbage_collect_worker_spec.rb | 78 +++ ...edule_merge_request_cleanup_refs_worker_spec.rb | 12 + .../wikis/git_garbage_collect_worker_spec.rb | 14 + 1076 files changed, 32232 insertions(+), 16325 deletions(-) create mode 100644 spec/benchmarks/banzai_benchmark.rb create mode 100644 spec/controllers/projects/security/configuration_controller_spec.rb create mode 100644 spec/experiments/members/invite_email_experiment_spec.rb create mode 100644 spec/factories/ci/reports/codequality_degradations.rb create mode 100644 spec/factories/packages/debian/group_component.rb create mode 100644 spec/factories/packages/debian/project_component.rb create mode 100644 spec/factories/token_with_ivs.rb delete mode 100644 spec/features/admin/admin_cohorts_spec.rb create mode 100644 spec/features/admin/admin_users_spec.rb create mode 100644 spec/features/projects/pages/user_adds_domain_spec.rb create mode 100644 spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb create mode 100644 spec/features/projects/pages/user_edits_settings_spec.rb delete mode 100644 spec/features/projects/pages_lets_encrypt_spec.rb delete mode 100644 spec/features/projects/pages_spec.rb create mode 100644 spec/features/whats_new_spec.rb create mode 100644 spec/finders/merge_request/metrics_finder_spec.rb create mode 100644 spec/finders/merge_requests/oldest_per_commit_finder_spec.rb create mode 100644 spec/finders/repositories/commits_with_trailer_finder_spec.rb create mode 100644 spec/finders/terraform/states_finder_spec.rb create mode 100644 spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json delete mode 100644 spec/fixtures/api/schemas/entities/group_group_link.json delete mode 100644 spec/fixtures/api/schemas/graphql/packages/package_composer_details.json create mode 100644 spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json delete mode 100644 spec/fixtures/api/schemas/group_group_links.json create mode 100644 spec/fixtures/api/schemas/group_link/group_group_link.json create mode 100644 spec/fixtures/api/schemas/group_link/group_group_links.json create mode 100644 spec/fixtures/api/schemas/group_link/group_link.json create mode 100644 spec/fixtures/api/schemas/group_link/project_group_link.json create mode 100644 spec/fixtures/api/schemas/group_link/project_group_links.json create mode 100644 spec/fixtures/packages/composer/package.json create mode 100644 spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json create mode 100644 spec/frontend/__helpers__/graphql_helpers.js create mode 100644 spec/frontend/__helpers__/graphql_helpers_spec.js create mode 100644 spec/frontend/admin/users/components/user_actions_spec.js create mode 100644 spec/frontend/admin/users/components/user_date_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_details_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_metrics_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_status_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_summary_row_spec.js delete mode 100644 spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js delete mode 100644 spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js delete mode 100644 spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js delete mode 100644 spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js delete mode 100644 spec/frontend/alert_management/mocks/alerts.json delete mode 100644 spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap delete mode 100644 spec/frontend/alerts_settings/alert_mapping_builder_spec.js delete mode 100644 spec/frontend/alerts_settings/alerts_integrations_list_spec.js delete mode 100644 spec/frontend/alerts_settings/alerts_settings_form_spec.js delete mode 100644 spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js create mode 100644 spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap create mode 100644 spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js create mode 100644 spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js create mode 100644 spec/frontend/alerts_settings/components/alerts_settings_form_spec.js create mode 100644 spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js create mode 100644 spec/frontend/alerts_settings/components/mocks/apollo_mock.js create mode 100644 spec/frontend/alerts_settings/components/mocks/integrations.json create mode 100644 spec/frontend/alerts_settings/components/util.js create mode 100644 spec/frontend/alerts_settings/mocks/alertFields.json delete mode 100644 spec/frontend/alerts_settings/mocks/apollo_mock.js delete mode 100644 spec/frontend/alerts_settings/mocks/integrations.json delete mode 100644 spec/frontend/alerts_settings/util.js create mode 100644 spec/frontend/alerts_settings/utils/mapping_transformations_spec.js create mode 100644 spec/frontend/boards/boards_util_spec.js create mode 100644 spec/frontend/boards/components/board_card_layout_deprecated_spec.js create mode 100644 spec/frontend/captcha/init_recaptcha_script_spec.js delete mode 100644 spec/frontend/groups/members/components/app_spec.js delete mode 100644 spec/frontend/groups/members/index_spec.js delete mode 100644 spec/frontend/groups/members/mock_data.js create mode 100644 spec/frontend/jira_connect/index_spec.js create mode 100644 spec/frontend/lib/utils/array_utility_spec.js create mode 100644 spec/frontend/members/components/app_spec.js create mode 100644 spec/frontend/members/index_spec.js create mode 100644 spec/frontend/notifications/components/notifications_dropdown_spec.js delete mode 100644 spec/frontend/onboarding_issues/index_spec.js delete mode 100644 spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap delete mode 100644 spec/frontend/packages/list/components/packages_filter_spec.js create mode 100644 spec/frontend/packages/list/components/packages_search_spec.js delete mode 100644 spec/frontend/packages/list/components/packages_sort_spec.js create mode 100644 spec/frontend/packages/list/components/tokens/package_type_token_spec.js create mode 100644 spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js create mode 100644 spec/frontend/packages_and_registries/settings/group/mock_data.js delete mode 100644 spec/frontend/pages/projects/edit/mount_search_settings_spec.js create mode 100644 spec/frontend/pipeline_editor/components/commit/commit_section_spec.js create mode 100644 spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js create mode 100644 spec/frontend/pipeline_editor/components/header/validation_segment_spec.js delete mode 100644 spec/frontend/pipeline_editor/components/info/validation_segment_spec.js create mode 100644 spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js create mode 100644 spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js create mode 100644 spec/frontend/pipeline_editor/pipeline_editor_home_spec.js create mode 100644 spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap create mode 100644 spec/frontend/pipelines/graph_shared/links_inner_spec.js create mode 100644 spec/frontend/pipelines/graph_shared/links_layer_spec.js delete mode 100644 spec/frontend/pipelines/legacy_header_component_spec.js delete mode 100644 spec/frontend/pipelines/shared/links_layer_spec.js create mode 100644 spec/frontend/projects/compare/components/app_spec.js create mode 100644 spec/frontend/projects/compare/components/revision_dropdown_spec.js create mode 100644 spec/frontend/projects/members/utils_spec.js delete mode 100644 spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js create mode 100644 spec/frontend/registry/explorer/components/delete_image_spec.js create mode 100644 spec/frontend/registry/explorer/components/details_page/empty_state_spec.js delete mode 100644 spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js create mode 100644 spec/frontend/search/sort/components/app_spec.js create mode 100644 spec/frontend/search/topbar/components/app_spec.js create mode 100644 spec/frontend/search_settings/mount_spec.js delete mode 100644 spec/frontend/search_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap create mode 100644 spec/frontend/vue_shared/alert_details/alert_details_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/alert_metrics_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/alert_status_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/mocks/alerts.json create mode 100644 spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js create mode 100644 spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js create mode 100644 spec/frontend/vue_shared/components/help_popover_spec.js create mode 100644 spec/frontend/vue_shared/components/runner_instructions/mock_data.js create mode 100644 spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js create mode 100644 spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/settings/settings_block_spec.js create mode 100644 spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb create mode 100644 spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/analyzers_entity_input_type_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/analyzers_entity_type_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/entity_input_type_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/entity_type_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/input_type_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/options_entity_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/type_spec.rb create mode 100644 spec/graphql/types/ci_configuration/sast/ui_component_size_enum_spec.rb delete mode 100644 spec/graphql/types/packages/composer/details_type_spec.rb create mode 100644 spec/graphql/types/packages/package_without_versions_type_spec.rb create mode 100644 spec/helpers/enable_search_settings_helper_spec.rb create mode 100644 spec/initializers/validate_puma_spec.rb create mode 100644 spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb delete mode 100644 spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb delete mode 100644 spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb create mode 100644 spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb create mode 100644 spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb create mode 100644 spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb create mode 100644 spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb create mode 100644 spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb create mode 100644 spec/lib/bulk_imports/pipeline/extracted_data_spec.rb create mode 100644 spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb create mode 100644 spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb delete mode 100644 spec/lib/gitlab/badge/coverage/metadata_spec.rb delete mode 100644 spec/lib/gitlab/badge/coverage/report_spec.rb delete mode 100644 spec/lib/gitlab/badge/coverage/template_spec.rb delete mode 100644 spec/lib/gitlab/badge/pipeline/metadata_spec.rb delete mode 100644 spec/lib/gitlab/badge/pipeline/status_spec.rb delete mode 100644 spec/lib/gitlab/badge/pipeline/template_spec.rb delete mode 100644 spec/lib/gitlab/badge/shared/metadata.rb create mode 100644 spec/lib/gitlab/changelog/committer_spec.rb create mode 100644 spec/lib/gitlab/changelog/config_spec.rb create mode 100644 spec/lib/gitlab/changelog/generator_spec.rb create mode 100644 spec/lib/gitlab/changelog/release_spec.rb create mode 100644 spec/lib/gitlab/changelog/template/compiler_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/coverage/report_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/coverage/template_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/pipeline/status_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/pipeline/template_spec.rb create mode 100644 spec/lib/gitlab/ci/badge/shared/metadata.rb create mode 100644 spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb create mode 100644 spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb delete mode 100644 spec/lib/gitlab/ci/build/credentials/registry_spec.rb create mode 100644 spec/lib/gitlab/ci/parsers/instrumentation_spec.rb create mode 100644 spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb create mode 100644 spec/lib/gitlab/ci/variables/helpers_spec.rb create mode 100644 spec/lib/gitlab/cluster/lifecycle_events_spec.rb create mode 100644 spec/lib/gitlab/composer/cache_spec.rb delete mode 100644 spec/lib/gitlab/danger/base_linter_spec.rb delete mode 100644 spec/lib/gitlab/danger/changelog_spec.rb delete mode 100644 spec/lib/gitlab/danger/commit_linter_spec.rb delete mode 100644 spec/lib/gitlab/danger/danger_spec_helper.rb delete mode 100644 spec/lib/gitlab/danger/emoji_checker_spec.rb delete mode 100644 spec/lib/gitlab/danger/helper_spec.rb delete mode 100644 spec/lib/gitlab/danger/merge_request_linter_spec.rb delete mode 100644 spec/lib/gitlab/danger/roulette_spec.rb delete mode 100644 spec/lib/gitlab/danger/sidekiq_queues_spec.rb delete mode 100644 spec/lib/gitlab/danger/teammate_spec.rb delete mode 100644 spec/lib/gitlab/danger/title_linting_spec.rb delete mode 100644 spec/lib/gitlab/danger/weightage/maintainers_spec.rb delete mode 100644 spec/lib/gitlab/danger/weightage/reviewers_spec.rb create mode 100644 spec/lib/gitlab/database/migration_helpers/v2_spec.rb create mode 100644 spec/lib/gitlab/diff/char_diff_spec.rb create mode 100644 spec/lib/gitlab/graphql/pagination/connections_spec.rb create mode 100644 spec/lib/gitlab/hook_data/group_builder_spec.rb create mode 100644 spec/lib/gitlab/hook_data/subgroup_builder_spec.rb create mode 100644 spec/lib/gitlab/metrics/subscribers/external_http_spec.rb create mode 100644 spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb create mode 100644 spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb create mode 100644 spec/lib/gitlab/terraform/state_migration_helper_spec.rb create mode 100644 spec/lib/gitlab/usage/docs/renderer_spec.rb create mode 100644 spec/lib/gitlab/usage/docs/value_formatter_spec.rb create mode 100644 spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb create mode 100644 spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb delete mode 100644 spec/lib/gitlab_danger_spec.rb create mode 100644 spec/lib/peek/views/external_http_spec.rb create mode 100644 spec/lib/security/ci_configuration/sast_build_actions_spec.rb create mode 100644 spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb create mode 100644 spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb create mode 100644 spec/migrations/add_has_external_issue_tracker_trigger_spec.rb create mode 100644 spec/models/packages/composer/cache_file_spec.rb create mode 100644 spec/models/packages/debian/group_component_spec.rb create mode 100644 spec/models/packages/debian/project_component_spec.rb delete mode 100644 spec/models/readme_blob_spec.rb create mode 100644 spec/models/token_with_iv_spec.rb create mode 100644 spec/models/u2f_registration_spec.rb create mode 100644 spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb delete mode 100644 spec/presenters/gitlab/whats_new/item_presenter_spec.rb create mode 100644 spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb delete mode 100644 spec/requests/api/graphql/packages/package_composer_details_spec.rb create mode 100644 spec/requests/api/graphql/packages/package_spec.rb create mode 100644 spec/requests/api/graphql/project/terraform/state_spec.rb create mode 100644 spec/requests/api/resource_access_tokens_spec.rb create mode 100644 spec/requests/groups/email_campaigns_controller_spec.rb create mode 100644 spec/requests/oauth/tokens_controller_spec.rb create mode 100644 spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb create mode 100644 spec/routing/projects/security/configuration_controller_routing_spec.rb create mode 100644 spec/rubocop/cop/gitlab/namespaced_class_spec.rb create mode 100644 spec/serializers/ci/codequality_mr_diff_entity_spec.rb create mode 100644 spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb create mode 100644 spec/serializers/ci/pipeline_entity_spec.rb create mode 100644 spec/serializers/diff_file_metadata_entity_spec.rb delete mode 100644 spec/serializers/group_group_link_entity_spec.rb delete mode 100644 spec/serializers/group_group_link_serializer_spec.rb create mode 100644 spec/serializers/group_link/group_group_link_entity_spec.rb create mode 100644 spec/serializers/group_link/group_group_link_serializer_spec.rb create mode 100644 spec/serializers/group_link/group_link_entity_spec.rb create mode 100644 spec/serializers/group_link/project_group_link_entity_spec.rb create mode 100644 spec/serializers/group_link/project_group_link_serializer_spec.rb delete mode 100644 spec/serializers/pipeline_entity_spec.rb create mode 100644 spec/services/captcha/captcha_verification_service_spec.rb create mode 100644 spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb create mode 100644 spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb create mode 100644 spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb create mode 100644 spec/services/discussions/unresolve_service_spec.rb create mode 100644 spec/services/groups/open_issues_count_service_spec.rb create mode 100644 spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb create mode 100644 spec/services/merge_requests/reload_merge_head_diff_service_spec.rb create mode 100644 spec/services/merge_requests/request_review_service_spec.rb create mode 100644 spec/services/namespaces/in_product_marketing_emails_service_spec.rb create mode 100644 spec/services/packages/debian/destroy_distribution_service_spec.rb create mode 100644 spec/services/packages/debian/update_distribution_service_spec.rb create mode 100644 spec/services/pages/migrate_from_legacy_storage_service_spec.rb create mode 100644 spec/services/repositories/changelog_service_spec.rb create mode 100644 spec/services/security/ci_configuration/sast_create_service_spec.rb create mode 100644 spec/services/security/ci_configuration/sast_parser_service_spec.rb create mode 100644 spec/support/matchers/pushed_frontend_feature_flags_matcher.rb create mode 100644 spec/support/matchers/track_self_describing_event_matcher.rb create mode 100644 spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb create mode 100644 spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb delete mode 100644 spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb delete mode 100644 spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb create mode 100644 spec/support/shared_examples/models/packages/debian/component_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb create mode 100644 spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb create mode 100644 spec/tasks/gitlab/password_rake_spec.rb create mode 100644 spec/tasks/gitlab/terraform/migrate_rake_spec.rb create mode 100644 spec/tooling/danger/base_linter_spec.rb create mode 100644 spec/tooling/danger/changelog_spec.rb create mode 100644 spec/tooling/danger/commit_linter_spec.rb create mode 100644 spec/tooling/danger/danger_spec_helper.rb create mode 100644 spec/tooling/danger/emoji_checker_spec.rb create mode 100644 spec/tooling/danger/feature_flag_spec.rb create mode 100644 spec/tooling/danger/helper_spec.rb create mode 100644 spec/tooling/danger/merge_request_linter_spec.rb create mode 100644 spec/tooling/danger/roulette_spec.rb create mode 100644 spec/tooling/danger/sidekiq_queues_spec.rb create mode 100644 spec/tooling/danger/teammate_spec.rb create mode 100644 spec/tooling/danger/title_linting_spec.rb create mode 100644 spec/tooling/danger/weightage/maintainers_spec.rb create mode 100644 spec/tooling/danger/weightage/reviewers_spec.rb create mode 100644 spec/tooling/gitlab_danger_spec.rb create mode 100644 spec/uploaders/packages/composer/cache_uploader_spec.rb create mode 100644 spec/validators/nested_attributes_duplicates_validator_spec.rb delete mode 100644 spec/validators/variable_duplicates_validator_spec.rb create mode 100644 spec/views/groups/show.html.haml_spec.rb create mode 100644 spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb create mode 100644 spec/views/projects/show.html.haml_spec.rb delete mode 100644 spec/views/projects/tree/_tree_row.html.haml_spec.rb delete mode 100644 spec/views/search/_filter.html.haml_spec.rb delete mode 100644 spec/views/search/_form.html.haml_spec.rb create mode 100644 spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb create mode 100644 spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb create mode 100644 spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb create mode 100644 spec/workers/projects/git_garbage_collect_worker_spec.rb create mode 100644 spec/workers/wikis/git_garbage_collect_worker_spec.rb (limited to 'spec') diff --git a/spec/benchmarks/banzai_benchmark.rb b/spec/benchmarks/banzai_benchmark.rb new file mode 100644 index 00000000000..e489237a2f2 --- /dev/null +++ b/spec/benchmarks/banzai_benchmark.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +if ENV.key?('BENCHMARK') + require 'spec_helper' + require 'erb' + require 'benchmark/ips' + + # This benchmarks some of the Banzai pipelines and filters. + # They are not definitive, but can be used by a developer to + # get a rough idea how the changing or addition of a new filter + # will effect performance. + # + # Run by: + # BENCHMARK=1 rspec spec/benchmarks/banzai_benchmark.rb + # or + # rake benchmark:banzai + # + RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do + include MarkupHelper + + let_it_be(:feature) { MarkdownFeature.new } + let_it_be(:project) { feature.project } + let_it_be(:group) { feature.group } + let_it_be(:wiki) { feature.wiki } + let_it_be(:wiki_page) { feature.wiki_page } + let_it_be(:markdown_text) { feature.raw_markdown } + + let!(:render_context) { Banzai::RenderContext.new(project, current_user) } + + before do + stub_application_setting(asset_proxy_enabled: true) + stub_application_setting(asset_proxy_secret_key: 'shared-secret') + stub_application_setting(asset_proxy_url: 'https://assets.example.com') + stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com)) + + Banzai::Filter::AssetProxyFilter.initialize_settings + end + + context 'pipelines' do + it 'benchmarks several pipelines' do + path = 'images/example.jpg' + gitaly_wiki_file = Gitlab::GitalyClient::WikiFile.new(path: path) + allow(wiki).to receive(:find_file).with(path).and_return(Gitlab::Git::WikiFile.new(gitaly_wiki_file)) + allow(wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } + + puts "\n--> Benchmarking Full, Wiki, and Plain pipelines\n" + + Benchmark.ips do |x| + x.config(time: 10, warmup: 2) + + x.report('Full pipeline') { markdown(markdown_text, { pipeline: :full }) } + x.report('Wiki pipeline') { markdown(markdown_text, { pipeline: :wiki, wiki: wiki, page_slug: wiki_page.slug }) } + x.report('Plain pipeline') { markdown(markdown_text, { pipeline: :plain_markdown }) } + + x.compare! + end + end + end + + context 'filters' do + let(:context) do + tmp = { project: project, current_user: current_user, render_context: render_context } + Banzai::Filter::AssetProxyFilter.transform_context(tmp) + end + + it 'benchmarks all filters in the FullPipeline' do + benchmark_pipeline_filters(:full) + end + + it 'benchmarks all filters in the PlainMarkdownPipeline' do + benchmark_pipeline_filters(:plain_markdown) + end + end + + # build up the source text for each filter + def build_filter_text(pipeline, initial_text) + filter_source = {} + input_text = initial_text + + pipeline.filters.each do |filter_klass| + filter_source[filter_klass] = input_text + + output = filter_klass.call(input_text, context) + input_text = output + end + + filter_source + end + + def benchmark_pipeline_filters(pipeline_type) + pipeline = Banzai::Pipeline[pipeline_type] + filter_source = build_filter_text(pipeline, markdown_text) + + puts "\n--> Benchmarking #{pipeline.name.demodulize} filters\n" + + Benchmark.ips do |x| + x.config(time: 10, warmup: 2) + + pipeline.filters.each do |filter_klass| + label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20) + + x.report(label) { filter_klass.call(filter_source[filter_klass], context) } + end + + x.compare! + end + end + + # Fake a `current_user` helper + def current_user + feature.user + end + end +end diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb index 9e7dfa043c3..68b37197ca7 100644 --- a/spec/config/object_store_settings_spec.rb +++ b/spec/config/object_store_settings_spec.rb @@ -49,6 +49,20 @@ RSpec.describe ObjectStoreSettings do } end + shared_examples 'consolidated settings for objects accelerated by Workhorse' do + it 'consolidates active object storage settings' do + described_class::WORKHORSE_ACCELERATED_TYPES.each do |object_type| + # Use to_h to avoid https://gitlab.com/gitlab-org/gitlab/-/issues/286873 + section = subject.try(object_type).to_h + + next unless section.dig('object_store', 'enabled') + + expect(section['object_store']['connection']).to eq(connection) + expect(section['object_store']['consolidated_settings']).to be true + end + end + end + it 'sets correct default values' do subject @@ -77,9 +91,7 @@ RSpec.describe ObjectStoreSettings do expect(settings.pages['object_store']['consolidated_settings']).to be true expect(settings.external_diffs['enabled']).to be false - expect(settings.external_diffs['object_store']['enabled']).to be false - expect(settings.external_diffs['object_store']['remote_directory']).to eq('external_diffs') - expect(settings.external_diffs['object_store']['consolidated_settings']).to be true + expect(settings.external_diffs['object_store']).to be_nil end it 'raises an error when a bucket is missing' do @@ -95,29 +107,50 @@ RSpec.describe ObjectStoreSettings do expect(settings.pages['object_store']).to eq(nil) end - it 'allows pages to define its own connection' do - pages_connection = { 'provider' => 'Google', 'google_application_default' => true } - config['pages'] = { - 'enabled' => true, - 'object_store' => { + context 'GitLab Pages' do + let(:pages_connection) { { 'provider' => 'Google', 'google_application_default' => true } } + + before do + config['pages'] = { 'enabled' => true, - 'connection' => pages_connection + 'object_store' => { + 'enabled' => true, + 'connection' => pages_connection + } } - } + end - expect { subject }.not_to raise_error + it_behaves_like 'consolidated settings for objects accelerated by Workhorse' - described_class::WORKHORSE_ACCELERATED_TYPES.each do |object_type| - section = settings.try(object_type) + it 'allows pages to define its own connection' do + expect { subject }.not_to raise_error - next unless section + expect(settings.pages['object_store']['connection']).to eq(pages_connection) + expect(settings.pages['object_store']['consolidated_settings']).to be_falsey + end + end - expect(section['object_store']['connection']).to eq(connection) - expect(section['object_store']['consolidated_settings']).to be true + context 'when object storage is disabled for artifacts with no bucket' do + before do + config['artifacts'] = { + 'enabled' => true, + 'object_store' => {} + } + config['object_store']['objects']['artifacts'] = { + 'enabled' => false + } end - expect(settings.pages['object_store']['connection']).to eq(pages_connection) - expect(settings.pages['object_store']['consolidated_settings']).to be_falsey + it_behaves_like 'consolidated settings for objects accelerated by Workhorse' + + it 'does not enable consolidated settings for artifacts' do + subject + + expect(settings.artifacts['enabled']).to be true + expect(settings.artifacts['object_store']['remote_directory']).to be_nil + expect(settings.artifacts['object_store']['enabled']).to be_falsey + expect(settings.artifacts['object_store']['consolidated_settings']).to be_falsey + end end context 'with legacy config' do diff --git a/spec/controllers/admin/cohorts_controller_spec.rb b/spec/controllers/admin/cohorts_controller_spec.rb index 9eb2a713517..77a9c8eb223 100644 --- a/spec/controllers/admin/cohorts_controller_spec.rb +++ b/spec/controllers/admin/cohorts_controller_spec.rb @@ -3,37 +3,15 @@ require 'spec_helper' RSpec.describe Admin::CohortsController do - context 'as admin' do - let(:user) { create(:admin) } + let(:user) { create(:admin) } - before do - sign_in(user) - end - - it 'renders 200' do - get :index - - expect(response).to have_gitlab_http_status(:success) - end - - describe 'GET #index' do - it_behaves_like 'tracking unique visits', :index do - let(:target_id) { 'i_analytics_cohorts' } - end - end + before do + sign_in(user) end - context 'as normal user' do - let(:user) { create(:user) } - - before do - sign_in(user) - end - - it 'renders a 404' do - get :index + it 'redirects to Overview->Users' do + get :index - expect(response).to have_gitlab_http_status(:not_found) - end + expect(response).to redirect_to(admin_users_path(tab: 'cohorts')) end end diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb index 3fffc50475c..cba25dbff95 100644 --- a/spec/controllers/admin/runners_controller_spec.rb +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -27,7 +27,8 @@ RSpec.describe Admin::RunnersController do # There is still an N+1 query for `runner.builds.count` # We also need to add 1 because it takes 2 queries to preload tags - expect { get :index }.not_to exceed_query_limit(control_count + 6) + # also looking for token nonce requires database queries + expect { get :index }.not_to exceed_query_limit(control_count + 16) expect(response).to have_gitlab_http_status(:ok) expect(response.body).to have_content('tag1') diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index f902a3d2541..6faec315eb6 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -29,6 +29,11 @@ RSpec.describe Admin::UsersController do expect(assigns(:users).first.association(:authorized_projects)).to be_loaded end + + it_behaves_like 'tracking unique visits', :index do + let(:target_id) { 'i_analytics_cohorts' } + let(:request_params) { { tab: 'cohorts' } } + end end describe 'GET :id' do @@ -180,7 +185,7 @@ RSpec.describe Admin::UsersController do it 'displays the error' do subject - expect(flash[:alert]).to eq('The user you are trying to approve is not pending an approval') + expect(flash[:alert]).to eq('The user you are trying to approve is not pending approval') end it 'does not activate the user' do diff --git a/spec/controllers/chaos_controller_spec.rb b/spec/controllers/chaos_controller_spec.rb index 550303d292a..cb4f12ff829 100644 --- a/spec/controllers/chaos_controller_spec.rb +++ b/spec/controllers/chaos_controller_spec.rb @@ -124,4 +124,23 @@ RSpec.describe ChaosController do expect(response).to have_gitlab_http_status(:ok) end end + + describe '#gc' do + let(:gc_stat) { GC.stat.stringify_keys } + + it 'runs a full GC on the current web worker' do + expect(Prometheus::PidProvider).to receive(:worker_id).and_return('worker-0') + expect(Gitlab::Chaos).to receive(:run_gc).and_return(gc_stat) + + post :gc + + expect(response).to have_gitlab_http_status(:ok) + expect(response_json['worker_id']).to eq('worker-0') + expect(response_json['gc_stat']).to eq(gc_stat) + end + end + + def response_json + Gitlab::Json.parse(response.body) + end end diff --git a/spec/controllers/concerns/spammable_actions_spec.rb b/spec/controllers/concerns/spammable_actions_spec.rb index 3b5b4d11a9b..25d5398c9da 100644 --- a/spec/controllers/concerns/spammable_actions_spec.rb +++ b/spec/controllers/concerns/spammable_actions_spec.rb @@ -6,21 +6,8 @@ RSpec.describe SpammableActions do controller(ActionController::Base) do include SpammableActions - # #create is used to test spammable_params - # for testing purposes - def create - spam_params = spammable_params - - # replace the actual request with a string in the JSON response, all we care is that it got set - spam_params[:request] = 'this is the request' if spam_params[:request] - - # just return the params in the response so they can be verified in this fake controller spec. - # Normally, they are processed further by the controller action - render json: spam_params.to_json, status: :ok - end - - # #update is used to test recaptcha_check_with_fallback - # for testing purposes + # #update is used here to test #recaptcha_check_with_fallback, but it could be invoked + # from #create or any other action which mutates a spammable via a controller. def update should_redirect = params[:should_redirect] == 'true' @@ -35,80 +22,7 @@ RSpec.describe SpammableActions do end before do - # Ordinarily we would not stub a method on the class under test, but :ensure_spam_config_loaded! - # returns false in the test environment, and is also strong_memoized, so we need to stub it - allow(controller).to receive(:ensure_spam_config_loaded!) { true } - end - - describe '#spammable_params' do - subject { post :create, format: :json, params: params } - - shared_examples 'expects request param only' do - it do - subject - - expect(response).to be_successful - expect(json_response).to eq({ 'request' => 'this is the request' }) - end - end - - shared_examples 'expects all spammable params' do - it do - subject - - expect(response).to be_successful - expect(json_response['request']).to eq('this is the request') - expect(json_response['recaptcha_verified']).to eq(true) - expect(json_response['spam_log_id']).to eq('1') - end - end - - let(:recaptcha_response) { nil } - let(:spam_log_id) { nil } - - context 'when recaptcha response is not present' do - let(:params) do - { - spam_log_id: spam_log_id - } - end - - it_behaves_like 'expects request param only' - end - - context 'when recaptcha response is present' do - let(:recaptcha_response) { 'abd123' } - let(:params) do - { - 'g-recaptcha-response': recaptcha_response, - spam_log_id: spam_log_id - } - end - - context 'when verify_recaptcha returns falsey' do - before do - expect(controller).to receive(:verify_recaptcha).with( - { - response: recaptcha_response - }) { false } - end - - it_behaves_like 'expects request param only' - end - - context 'when verify_recaptcha returns truthy' do - let(:spam_log_id) { 1 } - - before do - expect(controller).to receive(:verify_recaptcha).with( - { - response: recaptcha_response - }) { true } - end - - it_behaves_like 'expects all spammable params' - end - end + allow(Gitlab::Recaptcha).to receive(:load_configurations!) { true } end describe '#recaptcha_check_with_fallback' do @@ -154,12 +68,9 @@ RSpec.describe SpammableActions do allow(spammable).to receive(:valid?) { false } end - # NOTE: Not adding coverage of details of render_recaptcha?, the plan is to refactor it out - # of this module anyway as part of adding support for the GraphQL reCAPTCHA flow. - - context 'when render_recaptcha? is true' do + context 'when spammable.render_recaptcha? is true' do before do - expect(controller).to receive(:render_recaptcha?) { true } + expect(spammable).to receive(:render_recaptcha?) { true } end context 'when format is :html' do diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 39cbdfb9123..2056feb6434 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -130,7 +130,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do } end - it 'proxies status from the remote token request' do + it 'proxies status from the remote token request', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:service_unavailable) @@ -147,7 +147,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do } end - it 'proxies status from the remote manifest request' do + it 'proxies status from the remote manifest request', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:bad_request) @@ -156,15 +156,19 @@ RSpec.describe Groups::DependencyProxyForContainersController do end it 'sends a file' do - expect(controller).to receive(:send_file).with(manifest.file.path, {}) + expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type) subject end - it 'returns Content-Disposition: attachment' do + it 'returns Content-Disposition: attachment', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest) + expect(response.headers['Content-Length']).to eq(manifest.size) + expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION) + expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"") expect(response.headers['Content-Disposition']).to match(/^attachment/) end end @@ -207,7 +211,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do } end - it 'proxies status from the remote blob request' do + it 'proxies status from the remote blob request', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:bad_request) @@ -221,7 +225,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do subject end - it 'returns Content-Disposition: attachment' do + it 'returns Content-Disposition: attachment', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 4d78419e8eb..ff7a7f55863 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -225,6 +225,18 @@ RSpec.describe Groups::GroupMembersController do expect(requester.reload.expires_at).not_to eq(expires_at.to_date) end + + it 'returns error status' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it 'returns error message' do + subject + + expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' }) + end end context 'when set to a date in the future' do diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 939c36a98b2..9e5f68820d9 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -306,66 +306,6 @@ RSpec.describe GroupsController, factory_default: :keep do end end end - - describe 'tracking group creation for onboarding issues experiment' do - before do - sign_in(user) - end - - subject(:create_namespace) { post :create, params: { group: { name: 'new_group', path: 'new_group' } } } - - context 'experiment disabled' do - before do - stub_experiment(onboarding_issues: false) - end - - it 'does not track anything', :snowplow do - create_namespace - - expect_no_snowplow_event - end - end - - context 'experiment enabled' do - before do - stub_experiment(onboarding_issues: true) - end - - context 'and the user is part of the control group' do - before do - stub_experiment_for_subject(onboarding_issues: false) - end - - it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do - create_namespace - - expect_snowplow_event( - category: 'Growth::Conversion::Experiment::OnboardingIssues', - action: 'created_namespace', - label: anything, - property: 'control_group' - ) - end - end - - context 'and the user is part of the experimental group' do - before do - stub_experiment_for_subject(onboarding_issues: true) - end - - it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do - create_namespace - - expect_snowplow_event( - category: 'Growth::Conversion::Experiment::OnboardingIssues', - action: 'created_namespace', - label: anything, - property: 'experimental_group' - ) - end - end - end - end end describe 'GET #index' do diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb index d1c138617bb..9927ef0903a 100644 --- a/spec/controllers/import/bulk_imports_controller_spec.rb +++ b/spec/controllers/import/bulk_imports_controller_spec.rb @@ -59,7 +59,14 @@ RSpec.describe Import::BulkImportsController do parsed_response: [ { 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1', 'web_url' => 'http://demo.host/full/path/group1' }, { 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2', 'web_url' => 'http://demo.host/full/path/group1' } - ] + ], + headers: { + 'x-next-page' => '2', + 'x-page' => '1', + 'x-per-page' => '20', + 'x-total' => '37', + 'x-total-pages' => '2' + } ) end @@ -81,6 +88,17 @@ RSpec.describe Import::BulkImportsController do expect(json_response).to eq({ importable_data: client_response.parsed_response }.as_json) end + it 'forwards pagination headers' do + get :status, format: :json + + expect(response.headers['x-per-page']).to eq client_response.headers['x-per-page'] + expect(response.headers['x-page']).to eq client_response.headers['x-page'] + expect(response.headers['x-next-page']).to eq client_response.headers['x-next-page'] + expect(response.headers['x-prev-page']).to eq client_response.headers['x-prev-page'] + expect(response.headers['x-total']).to eq client_response.headers['x-total'] + expect(response.headers['x-total-pages']).to eq client_response.headers['x-total-pages'] + end + context 'when filtering' do it 'returns filtered result' do filter = 'test' diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index e863f5ef2fc..a8d38d12f23 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe InvitesController, :snowplow do +RSpec.describe InvitesController do let_it_be(:user) { create(:user) } let(:member) { create(:project_member, :invited, invite_email: user.email) } let(:raw_invite_token) { member.raw_invite_token } @@ -51,6 +51,28 @@ RSpec.describe InvitesController, :snowplow do end it_behaves_like 'invalid token' + + context 'when invite comes from the initial email invite' do + let(:params) { { id: raw_invite_token, invite_type: Members::InviteEmailExperiment::INVITE_TYPE } } + + it 'tracks via experiment', :aggregate_failures do + experiment = double(track: true) + allow(controller).to receive(:experiment).and_return(experiment) + + request + + expect(experiment).to have_received(:track).with(:opened) + expect(experiment).to have_received(:track).with(:accepted) + end + end + + context 'when invite does not come from initial email invite' do + it 'does not track via experiment' do + expect(controller).not_to receive(:experiment) + + request + end + end end context 'when not logged in' do @@ -82,6 +104,25 @@ RSpec.describe InvitesController, :snowplow do subject(:request) { post :accept, params: params } it_behaves_like 'invalid token' + + context 'when invite comes from the initial email invite' do + it 'tracks via experiment' do + experiment = double(track: true) + allow(controller).to receive(:experiment).and_return(experiment) + + post :accept, params: params, session: { invite_type: Members::InviteEmailExperiment::INVITE_TYPE } + + expect(experiment).to have_received(:track).with(:accepted) + end + end + + context 'when invite does not come from initial email invite' do + it 'does not track via experiment' do + expect(controller).not_to receive(:experiment) + + request + end + end end describe 'POST #decline for link in UI' do diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index f9d16e761cb..8a793e29bfa 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -186,6 +186,13 @@ RSpec.describe Projects::DiscussionsController do expect(Note.find(note.id).discussion.resolved?).to be false end + it "tracks thread unresolve usage data" do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_unresolve_thread_action).with(user: user) + + delete :unresolve, params: request_params + end + it "returns status 200" do delete :unresolve, params: request_params diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index e8b30294cdd..7da3d403b53 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -209,6 +209,13 @@ RSpec.describe Projects::ForksController do } end + let(:created_project) do + Namespace + .find_by_id(params[:namespace_key]) + .projects + .find_by_path(params.fetch(:path, project.path)) + end + subject do post :create, params: params end @@ -260,6 +267,21 @@ RSpec.describe Projects::ForksController do expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params)) end end + + context 'custom attributes set' do + let(:params) { super().merge(path: 'something_custom', name: 'Something Custom', description: 'Something Custom', visibility: 'private') } + + it 'creates a project with custom values' do + subject + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(namespace_project_import_path(user.namespace, params[:path])) + expect(created_project.path).to eq(params[:path]) + expect(created_project.name).to eq(params[:name]) + expect(created_project.description).to eq(params[:description]) + expect(created_project.visibility).to eq(params[:visibility]) + end + end end context 'when user is not signed in' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index d3bdf1baaae..81ffd2c4512 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -63,53 +63,20 @@ RSpec.describe Projects::IssuesController do end end - describe 'the null hypothesis experiment', :snowplow do - it 'defines the expected before actions' do - expect(controller).to use_before_action(:run_null_hypothesis_experiment) - end - - context 'when rolled out to 100%' do - it 'assigns the candidate experience and tracks the event' do - get :index, params: { namespace_id: project.namespace, project_id: project } - - expect_snowplow_event( - category: 'null_hypothesis', - action: 'index', - context: [{ - schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', - data: { variant: 'candidate', experiment: 'null_hypothesis', key: anything } - }] - ) - end + describe 'the null hypothesis experiment', :experiment do + before do + stub_experiments(null_hypothesis: :candidate) end - context 'when not rolled out' do - before do - stub_feature_flags(null_hypothesis: false) - end - - it 'assigns the control experience and tracks the event' do - get :index, params: { namespace_id: project.namespace, project_id: project } - - expect_snowplow_event( - category: 'null_hypothesis', - action: 'index', - context: [{ - schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', - data: { variant: 'control', experiment: 'null_hypothesis', key: anything } - }] - ) - end + it 'defines the expected before actions' do + expect(controller).to use_before_action(:run_null_hypothesis_experiment) end - context 'when gitlab_experiments is disabled' do - it 'does not run the experiment at all' do - stub_feature_flags(gitlab_experiments: false) + it 'assigns the candidate experience and tracks the event' do + expect(experiment(:null_hypothesis)).to track('index').on_any_instance.for(:candidate) + .with_context(project: project) - expect(controller).not_to receive(:run_null_hypothesis_experiment) - - get :index, params: { namespace_id: project.namespace, project_id: project } - end + get :index, params: { namespace_id: project.namespace, project_id: project } end end end @@ -1314,11 +1281,13 @@ RSpec.describe Projects::IssuesController do let!(:last_spam_log) { spam_logs.last } def post_verified_issue - post_new_issue({}, { spam_log_id: last_spam_log.id, 'g-recaptcha-response': true } ) + post_new_issue({}, { spam_log_id: last_spam_log.id, 'g-recaptcha-response': 'abc123' } ) end before do - expect(controller).to receive_messages(verify_recaptcha: true) + expect_next_instance_of(Captcha::CaptchaVerificationService) do |instance| + expect(instance).to receive(:execute) { true } + end end it 'accepts an issue after reCAPTCHA is verified' do diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index f54a07de853..50f8942d9d5 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -226,11 +226,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do let(:diffable_merge_ref) { true } it 'compares diffs with the head' do - MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request) - - expect(CompareService).to receive(:new).with( - project, merge_request.merge_ref_head.sha - ).and_call_original + create(:merge_request_diff, :merge_head, merge_request: merge_request) go(diff_head: true) @@ -242,8 +238,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do let(:diffable_merge_ref) { false } it 'compares diffs with the base' do - expect(CompareService).not_to receive(:new) - go(diff_head: true) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index cf8b4c564c4..c53dd1265e6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1118,6 +1118,108 @@ RSpec.describe Projects::MergeRequestsController do end end + describe 'GET codequality_mr_diff_reports' do + let_it_be(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + target_project: project, + source_project: project) + end + + let(:pipeline) do + create(:ci_pipeline, + :success, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + + before do + allow_any_instance_of(MergeRequest) + .to receive(:find_codequality_mr_diff_reports) + .and_return(report) + + allow_any_instance_of(MergeRequest) + .to receive(:actual_head_pipeline) + .and_return(pipeline) + end + + subject(:get_codequality_mr_diff_reports) do + get :codequality_mr_diff_reports, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json + end + + context 'permissions on a public project with private CI/CD' do + let(:project) { create :project, :repository, :public, :builds_private } + let(:report) { { status: :parsed, data: { 'files' => {} } } } + + context 'while signed out' do + before do + sign_out(user) + end + + it 'responds with a 404' do + get_codequality_mr_diff_reports + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to be_blank + end + end + + context 'while signed in as an unrelated user' do + before do + sign_in(create(:user)) + end + + it 'responds with a 404' do + get_codequality_mr_diff_reports + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to be_blank + end + end + end + + context 'when pipeline has jobs with codequality mr diff report' do + before do + allow_any_instance_of(MergeRequest) + .to receive(:has_codequality_mr_diff_report?) + .and_return(true) + end + + context 'when processing codequality mr diff report is in progress' do + let(:report) { { status: :parsing } } + + it 'sends polling interval' do + expect(Gitlab::PollingInterval).to receive(:set_header) + + get_codequality_mr_diff_reports + end + + it 'returns 204 HTTP status' do + get_codequality_mr_diff_reports + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when processing codequality mr diff report is completed' do + let(:report) { { status: :parsed, data: { 'files' => {} } } } + + it 'returns codequality mr diff report' do + get_codequality_mr_diff_reports + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'files' => {} }) + end + end + end + end + describe 'GET terraform_reports' do let_it_be(:merge_request) do create(:merge_request, diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index e96113c0133..6b77794c66d 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -150,7 +150,7 @@ RSpec.describe Projects::NotesController do end it 'returns an empty page of notes' do - expect(Gitlab::EtagCaching::Middleware).not_to receive(:skip!) + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) request.headers['X-Last-Fetched-At'] = microseconds(Time.zone.now) @@ -169,6 +169,8 @@ RSpec.describe Projects::NotesController do end it 'returns all notes' do + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) + get :index, params: request_params expect(json_response['notes'].count).to eq((page_1 + page_2 + page_3).size + 1) diff --git a/spec/controllers/projects/pipelines/tests_controller_spec.rb b/spec/controllers/projects/pipelines/tests_controller_spec.rb index 61118487e20..e6ff3a487ac 100644 --- a/spec/controllers/projects/pipelines/tests_controller_spec.rb +++ b/spec/controllers/projects/pipelines/tests_controller_spec.rb @@ -34,20 +34,38 @@ RSpec.describe Projects::Pipelines::TestsController do end describe 'GET #show.json' do - context 'when pipeline has build report results' do - let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) } + context 'when pipeline has builds with test reports' do + let(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) } + let(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') } let(:suite_name) { 'test' } let(:build_ids) { pipeline.latest_builds.pluck(:id) } + before do + build = main_pipeline.builds.last + build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window + + # The JUnit fixture for the given build has 3 failures. + # This service will create 1 test case failure record for each. + Ci::TestFailureHistoryService.new(main_pipeline).execute + end + it 'renders test suite data' do get_tests_show_json(build_ids) expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq('test') + + # Each test failure in this pipeline has a matching failure in the default branch + recent_failures = json_response['test_cases'].map { |tc| tc['recent_failures'] } + expect(recent_failures).to eq([ + { 'count' => 1, 'base_branch' => 'master' }, + { 'count' => 1, 'base_branch' => 'master' }, + { 'count' => 1, 'base_branch' => 'master' } + ]) end end - context 'when pipeline does not have build report results' do + context 'when pipeline has no builds that matches the given build_ids' do let(:pipeline) { create(:ci_empty_pipeline) } let(:suite_name) { 'test' } diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index d30cc8cbfd9..53a7c2ca069 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -325,6 +325,18 @@ RSpec.describe Projects::ProjectMembersController do expect(requester.reload.expires_at).not_to eq(expires_at.to_date) end + + it 'returns error status' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it 'returns error message' do + subject + + expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' }) + end end context 'when set to a date in the future' do diff --git a/spec/controllers/projects/security/configuration_controller_spec.rb b/spec/controllers/projects/security/configuration_controller_spec.rb new file mode 100644 index 00000000000..afbebbad3d1 --- /dev/null +++ b/spec/controllers/projects/security/configuration_controller_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Security::ConfigurationController do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET show' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: false) + end + + it 'renders not found' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when feature flag is enabled' do + context 'when user has guest access' do + before do + project.add_guest(user) + end + + it 'denies access' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when user has developer access' do + before do + project.add_developer(user) + end + + it 'grants access' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end + end +end diff --git a/spec/controllers/registrations/experience_levels_controller_spec.rb b/spec/controllers/registrations/experience_levels_controller_spec.rb index 015daba8682..a84c3257774 100644 --- a/spec/controllers/registrations/experience_levels_controller_spec.rb +++ b/spec/controllers/registrations/experience_levels_controller_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Registrations::ExperienceLevelsController do + include AfterNextHelpers + let_it_be(:namespace) { create(:group, path: 'group-path' ) } let_it_be(:user) { create(:user) } @@ -45,6 +47,9 @@ RSpec.describe Registrations::ExperienceLevelsController do end context 'with an authenticated user' do + let_it_be(:project) { build(:project, namespace: namespace, creator: user, path: 'project-path') } + let_it_be(:issues_board) { build(:board, id: 123, project: project) } + before do sign_in(user) stub_experiment_for_subject(onboarding_issues: true) @@ -85,91 +90,57 @@ RSpec.describe Registrations::ExperienceLevelsController do end end - describe 'redirection' do - let(:project) { build(:project, namespace: namespace, creator: user, path: 'project-path') } - let(:issues_board) { build(:board, id: 123, project: project) } + context 'when "Learn GitLab" project exists' do + let(:learn_gitlab_available?) { true } before do - stub_experiment_for_subject( - onboarding_issues: true, - default_to_issues_board: default_to_issues_board_xp? - ) allow_next_instance_of(LearnGitlab) do |learn_gitlab| allow(learn_gitlab).to receive(:available?).and_return(learn_gitlab_available?) allow(learn_gitlab).to receive(:project).and_return(project) allow(learn_gitlab).to receive(:board).and_return(issues_board) + allow(learn_gitlab).to receive(:label).and_return(double(id: 1)) end end - context 'when namespace_path param is missing' do - let(:params) { super().merge(namespace_path: nil) } - - where( - default_to_issues_board_xp?: [true, false], - learn_gitlab_available?: [true, false] - ) - - with_them do - it { is_expected.to redirect_to('/') } - end - end - - context 'when we have a namespace_path param' do - using RSpec::Parameterized::TableSyntax + context 'redirection' do + context 'when namespace_path param is missing' do + let(:params) { super().merge(namespace_path: nil) } - where(:default_to_issues_board_xp?, :learn_gitlab_available?, :path) do - true | true | '/group-path/project-path/-/boards/123' - true | false | '/group-path' - false | true | '/group-path' - false | false | '/group-path' - end + where( + learn_gitlab_available?: [true, false] + ) - with_them do - it { is_expected.to redirect_to(path) } - end - end - end - - describe 'applying the chosen level' do - context 'when a "Learn GitLab" project is available' do - before do - allow_next_instance_of(LearnGitlab) do |learn_gitlab| - allow(learn_gitlab).to receive(:available?).and_return(true) - allow(learn_gitlab).to receive(:label).and_return(double(id: 1)) + with_them do + it { is_expected.to redirect_to('/') } end end - context 'when novice' do - let(:params) { super().merge(experience_level: :novice) } + context 'when we have a namespace_path param' do + using RSpec::Parameterized::TableSyntax - it 'adds a BoardLabel' do - expect_next_instance_of(Boards::UpdateService) do |service| - expect(service).to receive(:execute) - end - - subject + where(:learn_gitlab_available?, :path) do + true | '/group-path/project-path/-/boards/123' + false | '/group-path' end - end - - context 'when experienced' do - let(:params) { super().merge(experience_level: :experienced) } - it 'does not add a BoardLabel' do - expect(Boards::UpdateService).not_to receive(:new) - - subject + with_them do + it { is_expected.to redirect_to(path) } end end end - context 'when no "Learn GitLab" project exists' do + context 'when novice' do let(:params) { super().merge(experience_level: :novice) } - before do - allow_next_instance_of(LearnGitlab) do |learn_gitlab| - allow(learn_gitlab).to receive(:available?).and_return(false) - end + it 'adds a BoardLabel' do + expect_next(Boards::UpdateService).to receive(:execute) + + subject end + end + + context 'when experienced' do + let(:params) { super().merge(experience_level: :experienced) } it 'does not add a BoardLabel' do expect(Boards::UpdateService).not_to receive(:new) @@ -178,6 +149,20 @@ RSpec.describe Registrations::ExperienceLevelsController do end end end + + context 'when no "Learn GitLab" project exists' do + let(:params) { super().merge(experience_level: :novice) } + + before do + allow_next(LearnGitlab).to receive(:available?).and_return(false) + end + + it 'does not add a BoardLabel' do + expect(Boards::UpdateService).not_to receive(:new) + + subject + end + end end context 'when user update fails' do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 737ec4f95c5..aac7c10d878 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -73,6 +73,18 @@ RSpec.describe RegistrationsController do end end end + + context 'audit events' do + context 'when not licensed' do + before do + stub_licensed_features(admin_audit_log: false) + end + + it 'does not log any audit event' do + expect { subject }.not_to change(AuditEvent, :count) + end + end + end end context 'when the `require_admin_approval_after_user_signup` setting is turned off' do diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 34052496871..bd5d07fda71 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -52,12 +52,10 @@ RSpec.describe Repositories::GitHttpController do }.from(0).to(1) end - it 'records an onboarding progress action' do - expect_next_instance_of(OnboardingProgressService) do |service| - expect(service).to receive(:execute).with(action: :git_read) - end + it_behaves_like 'records an onboarding progress action', :git_read do + let(:namespace) { project.namespace } - send_request + subject { send_request } end end end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index bbd39fd4c83..c531c699e98 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -5,278 +5,296 @@ require 'spec_helper' RSpec.describe SearchController do include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } + context 'authorized user' do + let(:user) { create(:user) } - before do - sign_in(user) - end - - shared_examples_for 'when the user cannot read cross project' do |action, params| before do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?) - .with(user, :read_cross_project, :global) { false } + sign_in(user) end - it 'blocks access without a project_id' do - get action, params: params - - expect(response).to have_gitlab_http_status(:forbidden) - end + shared_examples_for 'when the user cannot read cross project' do |action, params| + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_cross_project, :global) { false } + end - it 'allows access with a project_id' do - get action, params: params.merge(project_id: create(:project, :public).id) + it 'blocks access without a project_id' do + get action, params: params - expect(response).to have_gitlab_http_status(:ok) - end - end + expect(response).to have_gitlab_http_status(:forbidden) + end - shared_examples_for 'with external authorization service enabled' do |action, params| - let(:project) { create(:project, namespace: user.namespace) } - let(:note) { create(:note_on_issue, project: project) } + it 'allows access with a project_id' do + get action, params: params.merge(project_id: create(:project, :public).id) - before do - enable_external_authorization_service_check + expect(response).to have_gitlab_http_status(:ok) + end end - it 'renders a 403 when no project is given' do - get action, params: params + shared_examples_for 'with external authorization service enabled' do |action, params| + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } - expect(response).to have_gitlab_http_status(:forbidden) - end + before do + enable_external_authorization_service_check + end - it 'renders a 200 when a project was set' do - get action, params: params.merge(project_id: project.id) + it 'renders a 403 when no project is given' do + get action, params: params - expect(response).to have_gitlab_http_status(:ok) - end - end + expect(response).to have_gitlab_http_status(:forbidden) + end - describe 'GET #show' do - it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do - it 'still allows accessing the search page' do - get :show + it 'renders a 200 when a project was set' do + get action, params: params.merge(project_id: project.id) expect(response).to have_gitlab_http_status(:ok) end end - it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' } + describe 'GET #show' do + it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do + it 'still allows accessing the search page' do + get :show - context 'uses the right partials depending on scope' do - using RSpec::Parameterized::TableSyntax - render_views - - let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) } - - before do - expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + expect(response).to have_gitlab_http_status(:ok) + end end - subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) } + it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' } - where(:partial, :scope) do - '_blob' | :blobs - '_wiki_blob' | :wiki_blobs - '_commit' | :commits - end + context 'uses the right partials depending on scope' do + using RSpec::Parameterized::TableSyntax + render_views - with_them do - it do - project_wiki = create(:project_wiki, project: project, user: user) - create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge') + let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) } - expect(subject).to render_template("search/results/#{partial}") + before do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original end - end - end - context 'global search' do - using RSpec::Parameterized::TableSyntax - render_views + subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) } - context 'when block_anonymous_global_searches is disabled' do - before do - stub_feature_flags(block_anonymous_global_searches: false) + where(:partial, :scope) do + '_blob' | :blobs + '_wiki_blob' | :wiki_blobs + '_commit' | :commits end - it 'omits pipeline status from load' do - project = create(:project, :public) - expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects) + with_them do + it do + project_wiki = create(:project_wiki, project: project, user: user) + create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge') - get :show, params: { scope: 'projects', search: project.name } - - expect(assigns[:search_objects].first).to eq project + expect(subject).to render_template("search/results/#{partial}") + end end + end - context 'check search term length' do - let(:search_queries) do - char_limit = SearchService::SEARCH_CHAR_LIMIT - term_limit = SearchService::SEARCH_TERM_LIMIT - { - chars_under_limit: ('a' * (char_limit - 1)), - chars_over_limit: ('a' * (char_limit + 1)), - terms_under_limit: ('abc ' * (term_limit - 1)), - terms_over_limit: ('abc ' * (term_limit + 1)) - } + context 'global search' do + using RSpec::Parameterized::TableSyntax + render_views + + context 'when block_anonymous_global_searches is disabled' do + before do + stub_feature_flags(block_anonymous_global_searches: false) end - where(:string_name, :expectation) do - :chars_under_limit | :not_to_set_flash - :chars_over_limit | :set_chars_flash - :terms_under_limit | :not_to_set_flash - :terms_over_limit | :set_terms_flash + it 'omits pipeline status from load' do + project = create(:project, :public) + expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects) + + get :show, params: { scope: 'projects', search: project.name } + + expect(assigns[:search_objects].first).to eq project end - with_them do - it do - get :show, params: { scope: 'projects', search: search_queries[string_name] } - - case expectation - when :not_to_set_flash - expect(controller).not_to set_flash[:alert] - when :set_chars_flash - expect(controller).to set_flash[:alert].to(/characters/) - when :set_terms_flash - expect(controller).to set_flash[:alert].to(/terms/) + context 'check search term length' do + let(:search_queries) do + char_limit = SearchService::SEARCH_CHAR_LIMIT + term_limit = SearchService::SEARCH_TERM_LIMIT + { + chars_under_limit: ('a' * (char_limit - 1)), + chars_over_limit: ('a' * (char_limit + 1)), + terms_under_limit: ('abc ' * (term_limit - 1)), + terms_over_limit: ('abc ' * (term_limit + 1)) + } + end + + where(:string_name, :expectation) do + :chars_under_limit | :not_to_set_flash + :chars_over_limit | :set_chars_flash + :terms_under_limit | :not_to_set_flash + :terms_over_limit | :set_terms_flash + end + + with_them do + it do + get :show, params: { scope: 'projects', search: search_queries[string_name] } + + case expectation + when :not_to_set_flash + expect(controller).not_to set_flash[:alert] + when :set_chars_flash + expect(controller).to set_flash[:alert].to(/characters/) + when :set_terms_flash + expect(controller).to set_flash[:alert].to(/terms/) + end end end end end - end - context 'when block_anonymous_global_searches is enabled' do - context 'for unauthenticated user' do - before do - sign_out(user) - end + context 'when block_anonymous_global_searches is enabled' do + context 'for unauthenticated user' do + before do + sign_out(user) + end - it 'redirects to login page' do - get :show, params: { scope: 'projects', search: '*' } + it 'redirects to login page' do + get :show, params: { scope: 'projects', search: '*' } - expect(response).to redirect_to new_user_session_path + expect(response).to redirect_to new_user_session_path + end end - end - context 'for authenticated user' do - it 'succeeds' do - get :show, params: { scope: 'projects', search: '*' } + context 'for authenticated user' do + it 'succeeds' do + get :show, params: { scope: 'projects', search: '*' } - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:ok) + end end end end - end - it 'finds issue comments' do - project = create(:project, :public) - note = create(:note_on_issue, project: project) + it 'finds issue comments' do + project = create(:project, :public) + note = create(:note_on_issue, project: project) - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - - expect(assigns[:search_objects].first).to eq note - end + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - context 'unique users tracking' do - before do - allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + expect(assigns[:search_objects].first).to eq note end - it_behaves_like 'tracking unique hll events', :search_track_unique_users do - subject(:request) { get :show, params: { scope: 'projects', search: 'term' } } + context 'unique users tracking' do + before do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + end + + it_behaves_like 'tracking unique hll events', :search_track_unique_users do + subject(:request) { get :show, params: { scope: 'projects', search: 'term' } } - let(:target_id) { 'i_search_total' } - let(:expected_type) { instance_of(String) } + let(:target_id) { 'i_search_total' } + let(:expected_type) { instance_of(String) } + end end - end - context 'on restricted projects' do - context 'when signed out' do - before do - sign_out(user) + context 'on restricted projects' do + context 'when signed out' do + before do + sign_out(user) + end + + it "doesn't expose comments on issues" do + project = create(:project, :public, :issues_private) + note = create(:note_on_issue, project: project) + + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + + expect(assigns[:search_objects].count).to eq(0) + end end - it "doesn't expose comments on issues" do - project = create(:project, :public, :issues_private) - note = create(:note_on_issue, project: project) + it "doesn't expose comments on merge_requests" do + project = create(:project, :public, :merge_requests_private) + note = create(:note_on_merge_request, project: project) get :show, params: { project_id: project.id, scope: 'notes', search: note.note } expect(assigns[:search_objects].count).to eq(0) end - end - it "doesn't expose comments on merge_requests" do - project = create(:project, :public, :merge_requests_private) - note = create(:note_on_merge_request, project: project) + it "doesn't expose comments on snippets" do + project = create(:project, :public, :snippets_private) + note = create(:note_on_project_snippet, project: project) - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - expect(assigns[:search_objects].count).to eq(0) + expect(assigns[:search_objects].count).to eq(0) + end end + end - it "doesn't expose comments on snippets" do - project = create(:project, :public, :snippets_private) - note = create(:note_on_project_snippet, project: project) - - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + describe 'GET #count' do + it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } + it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } - expect(assigns[:search_objects].count).to eq(0) - end - end - end + it 'returns the result count for the given term and scope' do + create(:project, :public, name: 'hello world') + create(:project, :public, name: 'foo bar') - describe 'GET #count' do - it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } - it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } + get :count, params: { search: 'hello', scope: 'projects' } - it 'returns the result count for the given term and scope' do - create(:project, :public, name: 'hello world') - create(:project, :public, name: 'foo bar') + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'count' => '1' }) + end - get :count, params: { search: 'hello', scope: 'projects' } + it 'raises an error if search term is missing' do + expect do + get :count, params: { scope: 'projects' } + end.to raise_error(ActionController::ParameterMissing) + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'count' => '1' }) + it 'raises an error if search scope is missing' do + expect do + get :count, params: { search: 'hello' } + end.to raise_error(ActionController::ParameterMissing) + end end - it 'raises an error if search term is missing' do - expect do - get :count, params: { scope: 'projects' } - end.to raise_error(ActionController::ParameterMissing) + describe 'GET #autocomplete' do + it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } + it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } end - it 'raises an error if search scope is missing' do - expect do - get :count, params: { search: 'hello' } - end.to raise_error(ActionController::ParameterMissing) - end - end + describe '#append_info_to_payload' do + it 'appends search metadata for logging' do + last_payload = nil + original_append_info_to_payload = controller.method(:append_info_to_payload) - describe 'GET #autocomplete' do - it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } - it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } - end + expect(controller).to receive(:append_info_to_payload) do |payload| + original_append_info_to_payload.call(payload) + last_payload = payload + end - describe '#append_info_to_payload' do - it 'appends search metadata for logging' do - last_payload = nil - original_append_info_to_payload = controller.method(:append_info_to_payload) + get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true } - expect(controller).to receive(:append_info_to_payload) do |payload| - original_append_info_to_payload.call(payload) - last_payload = payload + expect(last_payload[:metadata]['meta.search.group_id']).to eq('123') + expect(last_payload[:metadata]['meta.search.project_id']).to eq('456') + expect(last_payload[:metadata]).not_to have_key('meta.search.search') + expect(last_payload[:metadata]['meta.search.scope']).to eq('issues') + expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true') + expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true') + expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true') end + end + end - get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true } + context 'unauthorized user' do + describe 'GET #opensearch' do + render_views + + it 'renders xml' do + get :opensearch, format: :xml + + doc = Nokogiri::XML.parse(response.body) - expect(last_payload[:metadata]['meta.search.group_id']).to eq('123') - expect(last_payload[:metadata]['meta.search.project_id']).to eq('456') - expect(last_payload[:metadata]).not_to have_key('meta.search.search') - expect(last_payload[:metadata]['meta.search.scope']).to eq('issues') - expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true') - expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true') - expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true') + expect(response).to have_gitlab_http_status(:ok) + expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab') + expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm]) + end end end end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 3087beb1326..efbf6e7baab 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -184,6 +184,7 @@ RSpec.describe 'Database schema' do "ApplicationSetting" => %w[repository_storages_weighted], "AlertManagement::Alert" => %w[payload], "Ci::BuildMetadata" => %w[config_options config_variables], + "ExperimentSubject" => %w[context], "ExperimentUser" => %w[context], "Geo::Event" => %w[payload], "GeoNodeStatus" => %w[status], diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb index bc90f67f0db..f66458cfbfb 100644 --- a/spec/deprecation_toolkit_env.rb +++ b/spec/deprecation_toolkit_env.rb @@ -1,18 +1,89 @@ # frozen_string_literal: true -if ENV.key?('RECORD_DEPRECATIONS') - require 'deprecation_toolkit' - require 'deprecation_toolkit/rspec' - DeprecationToolkit::Configuration.test_runner = :rspec - DeprecationToolkit::Configuration.deprecation_path = 'deprecations' - DeprecationToolkit::Configuration.behavior = DeprecationToolkit::Behaviors::Record - - # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2 - Warning[:deprecated] = true - - kwargs_warnings = [ - # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18 +require 'deprecation_toolkit' +require 'deprecation_toolkit/rspec' + +module DeprecationToolkitEnv + module DeprecationBehaviors + class SelectiveRaise + attr_reader :disallowed_deprecations_proc + + class RaiseDisallowedDeprecation < StandardError + def initialize(test, current_deprecations) + message = <<~EOF + Disallowed deprecations detected while running test #{test}: + + #{current_deprecations.deprecations.join("\n")} + EOF + + super(message) + end + end + + def initialize(disallowed_deprecations_proc) + @disallowed_deprecations_proc = disallowed_deprecations_proc + end + + # Note: trigger does not get called if the current_deprecations matches recorded_deprecations + # See https://github.com/Shopify/deprecation_toolkit/blob/2398f38acb62220fb79a6cd720f61d9cea26bc06/lib/deprecation_toolkit/test_triggerer.rb#L8-L11 + def trigger(test, current_deprecations, recorded_deprecations) + if selected_for_raise?(current_deprecations) + raise RaiseDisallowedDeprecation.new(test, current_deprecations) + elsif ENV['RECORD_DEPRECATIONS'] + record(test, current_deprecations, recorded_deprecations) + end + end + + private + + def selected_for_raise?(current_deprecations) + disallowed_deprecations_proc.call(current_deprecations.deprecations_without_stacktrace) + end + + def record(test, current_deprecations, recorded_deprecations) + ::DeprecationToolkit::Behaviors::Record.trigger(test, current_deprecations, recorded_deprecations) + end + end + end + + # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18 + def self.kwargs_warning %r{warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z} - ] - DeprecationToolkit::Configuration.warnings_treated_as_deprecation = kwargs_warnings + end + + # Allow these Gem paths to trigger keyword warnings as we upgrade these gems + # one by one + def self.allowed_kwarg_warning_paths + %w[ + spec/support/gitlab_experiment.rb + activerecord-6.0.3.4/lib/active_record/migration.rb + devise-4.7.3/lib/devise/test/controller_helpers.rb + grape-1.5.1/lib/grape/middleware/stack.rb + grape-1.5.1/lib/grape/validations/validators/coerce.rb + grape_logging-1.8.3/lib/grape_logging/middleware/request_logger.rb + activesupport-6.0.3.4/lib/active_support/cache.rb + factory_bot-6.1.0/lib/factory_bot/decorator.rb + batch-loader-1.4.0/lib/batch_loader/graphql.rb + carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb + activerecord-6.0.3.4/lib/active_record/relation.rb + ] + end + + def self.configure! + # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2 + Warning[:deprecated] = true + + DeprecationToolkit::Configuration.test_runner = :rspec + DeprecationToolkit::Configuration.deprecation_path = 'deprecations' + DeprecationToolkit::Configuration.warnings_treated_as_deprecation = [kwargs_warning] + + disallowed_deprecations = -> (deprecations) do + deprecations.any? do |deprecation| + kwargs_warning.match?(deprecation) && + allowed_kwarg_warning_paths.none? { |path| deprecation.include?(path) } + end + end + + DeprecationToolkit::Configuration.behavior = DeprecationBehaviors::SelectiveRaise.new(disallowed_deprecations) + end end diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb index ece52d37351..6b4a3ece59e 100644 --- a/spec/experiments/application_experiment_spec.rb +++ b/spec/experiments/application_experiment_spec.rb @@ -2,8 +2,60 @@ require 'spec_helper' -RSpec.describe ApplicationExperiment do - subject { described_class.new(:stub) } +RSpec.describe ApplicationExperiment, :experiment do + subject { described_class.new('namespaced/stub') } + + let(:feature_definition) do + { name: 'namespaced_stub', type: 'experiment', group: 'group::adoption', default_enabled: false } + end + + around do |example| + Feature::Definition.definitions[:namespaced_stub] = Feature::Definition.new('namespaced_stub.yml', feature_definition) + example.run + Feature::Definition.definitions.delete(:namespaced_stub) + end + + before do + allow(subject).to receive(:enabled?).and_return(true) + end + + it "naively assumes a 1x1 relationship to feature flags for tests" do + expect(Feature).to receive(:persist_used!).with('namespaced_stub') + + described_class.new('namespaced/stub') + end + + describe "enabled" do + before do + allow(subject).to receive(:enabled?).and_call_original + + allow(Feature::Definition).to receive(:get).and_return('_instance_') + allow(Gitlab).to receive(:dev_env_or_com?).and_return(true) + allow(Feature).to receive(:get).and_return(double(state: :on)) + end + + it "is enabled when all criteria are met" do + expect(subject).to be_enabled + end + + it "isn't enabled if the feature definition doesn't exist" do + expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil) + + expect(subject).not_to be_enabled + end + + it "isn't enabled if we're not in dev or dotcom environments" do + expect(Gitlab).to receive(:dev_env_or_com?).and_return(false) + + expect(subject).not_to be_enabled + end + + it "isn't enabled if the feature flag state is :off" do + expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off)) + + expect(subject).not_to be_enabled + end + end describe "publishing results" do it "tracks the assignment" do @@ -16,9 +68,9 @@ RSpec.describe ApplicationExperiment do expect(Gon.global).to receive(:push).with( { experiment: { - 'stub' => { # string key because it can be namespaced - experiment: 'stub', - key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', + 'namespaced/stub' => { # string key because it can be namespaced + experiment: 'namespaced/stub', + key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' } } @@ -31,8 +83,8 @@ RSpec.describe ApplicationExperiment do end describe "tracking events", :snowplow do - it "doesn't track if excluded" do - subject.exclude { true } + it "doesn't track if we shouldn't track" do + allow(subject).to receive(:should_track?).and_return(false) subject.track(:action) @@ -45,7 +97,7 @@ RSpec.describe ApplicationExperiment do ]) expect_snowplow_event( - category: 'stub', + category: 'namespaced/stub', action: 'action', property: '_property_', context: [ @@ -55,7 +107,7 @@ RSpec.describe ApplicationExperiment do }, { schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', - data: { experiment: 'stub', key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', variant: 'control' } + data: { experiment: 'namespaced/stub', key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' } } ] ) @@ -63,8 +115,14 @@ RSpec.describe ApplicationExperiment do end describe "variant resolution" do + it "uses the default value as specified in the yaml" do + expect(Feature).to receive(:enabled?).with('namespaced_stub', subject, type: :experiment, default_enabled: :yaml) + + expect(subject.variant.name).to eq('control') + end + it "returns nil when not rolled out" do - stub_feature_flags(stub: false) + stub_feature_flags(namespaced_stub: false) expect(subject.variant.name).to eq('control') end @@ -105,7 +163,7 @@ RSpec.describe ApplicationExperiment do # every control variant assigned, we'd inflate the cache size and # wouldn't be able to roll out to subjects that we'd already assigned to # the control. - stub_feature_flags(stub: false) # simulate being not rolled out + stub_feature_flags(namespaced_stub: false) # simulate being not rolled out expect(subject.variant.name).to eq('control') # if we ask, it should be control diff --git a/spec/experiments/members/invite_email_experiment_spec.rb b/spec/experiments/members/invite_email_experiment_spec.rb new file mode 100644 index 00000000000..b8ef502382c --- /dev/null +++ b/spec/experiments/members/invite_email_experiment_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Members::InviteEmailExperiment do + subject do + experiment('members/invite_email', actor: double('Member', created_by: double('User', avatar_url: '_avatar_url_'))) + end + + before do + allow(subject).to receive(:enabled?).and_return(true) + end + + describe "variant resolution" do + it "returns nil when not rolled out" do + stub_feature_flags(members_invite_email: false) + + expect(subject.variant.name).to eq('control') + end + + context "when rolled out to 100%" do + it "returns the first variant name" do + subject.try(:avatar) {} + + expect(subject.variant.name).to eq('avatar') + end + end + end + + describe "exclusions", :experiment do + it "excludes when created by is nil" do + expect(experiment('members/invite_email')).to exclude(actor: double(created_by: nil)) + end + + it "excludes when avatar_url is nil" do + member_without_avatar_url = double('Member', created_by: double('User', avatar_url: nil)) + + expect(experiment('members/invite_email')).to exclude(actor: member_without_avatar_url) + end + end +end diff --git a/spec/factories/audit_events.rb b/spec/factories/audit_events.rb index 4e72976a9e5..05b86d2f13b 100644 --- a/spec/factories/audit_events.rb +++ b/spec/factories/audit_events.rb @@ -49,6 +49,21 @@ FactoryBot.define do end end + trait :unauthenticated do + author_id { -1 } + details do + { + custom_message: 'Custom action', + author_name: 'An unauthenticated user', + target_id: target_project.id, + target_type: 'Project', + target_details: target_project.name, + ip_address: '127.0.0.1', + entity_path: target_project.full_path + } + end + end + trait :group_event do transient { target_group { association(:group) } } diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index 7727a468633..7258b3367d3 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -53,6 +53,11 @@ FactoryBot.define do finished_at { '2013-10-29 09:53:28 CET' } end + trait :success do + finished + status { 'success' } + end + trait :failed do finished status { 'failed' } @@ -75,5 +80,9 @@ FactoryBot.define do trait :playable do manual end + + trait :allowed_to_fail do + allow_failure { true } + end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 24abad66530..c85918a3187 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -308,12 +308,6 @@ FactoryBot.define do end end - trait :codequality_report do - after(:build) do |build| - build.job_artifacts << create(:ci_job_artifact, :codequality, job: build) - end - end - trait :test_reports do after(:build) do |build| build.job_artifacts << create(:ci_job_artifact, :junit, job: build) diff --git a/spec/factories/ci/pipeline_artifacts.rb b/spec/factories/ci/pipeline_artifacts.rb index fa33609dd6c..05ff7afed7c 100644 --- a/spec/factories/ci/pipeline_artifacts.rb +++ b/spec/factories/ci/pipeline_artifacts.rb @@ -4,18 +4,30 @@ FactoryBot.define do factory :ci_pipeline_artifact, class: 'Ci::PipelineArtifact' do pipeline factory: :ci_pipeline project { pipeline.project } - file_type { :code_coverage } file_format { :raw } file_store { ObjectStorage::SUPPORTED_STORES.first } - size { 1.megabytes } - + size { 1.megabyte } + file_type { :code_coverage } after(:build) do |artifact, _evaluator| artifact.file = fixture_file_upload( Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json') end - trait :with_multibyte_characters do + trait :with_coverage_report do + file_type { :code_coverage } + + after(:build) do |artifact, _evaluator| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json') + end + + size { file.size } + end + + trait :with_coverage_multibyte_characters do + file_type { :code_coverage } size { { "utf8" => "✓" }.to_json.bytesize } + after(:build) do |artifact, _evaluator| artifact.file = CarrierWaveStringFile.new_file( file_content: { "utf8" => "✓" }.to_json, @@ -26,12 +38,25 @@ FactoryBot.define do end trait :with_code_coverage_with_multiple_files do + file_type { :code_coverage } + after(:build) do |artifact, _evaluator| artifact.file = fixture_file_upload( Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage_with_multiple_files.json'), 'application/json' ) end + size { 1.megabyte } + end + + trait :with_codequality_mr_diff_report do + file_type { :code_quality_mr_diff } + + after(:build) do |artifact, _evaluator| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json'), 'application/json') + end + size { file.size } end end diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 86a8b008e48..530bb0cd25b 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -93,14 +93,6 @@ FactoryBot.define do end end - trait :with_codequality_report do - status { :success } - - after(:build) do |pipeline, evaluator| - pipeline.builds << build(:ci_build, :codequality_report, pipeline: pipeline, project: pipeline.project) - end - end - trait :with_test_reports do status { :success } @@ -159,7 +151,13 @@ FactoryBot.define do trait :with_coverage_report_artifact do after(:build) do |pipeline, evaluator| - pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, pipeline: pipeline, project: pipeline.project) + pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, :with_coverage_report, pipeline: pipeline, project: pipeline.project) + end + end + + trait :with_codequality_mr_diff_report do + after(:build) do |pipeline, evaluator| + pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, :with_codequality_mr_diff_report, pipeline: pipeline, project: pipeline.project) end end diff --git a/spec/factories/ci/reports/codequality_degradations.rb b/spec/factories/ci/reports/codequality_degradations.rb new file mode 100644 index 00000000000..d82157b457a --- /dev/null +++ b/spec/factories/ci/reports/codequality_degradations.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :codequality_degradation_1, class: Hash do + skip_create + + initialize_with do + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "file_a.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + }.with_indifferent_access + end + end + + factory :codequality_degradation_2, class: Hash do + skip_create + + initialize_with do + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "f3bdc1e8c102ba5fbd9e7f6cda51c95e", + "location": { + "path": "file_a.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + }.with_indifferent_access + end + end + + factory :codequality_degradation_3, class: Hash do + skip_create + + initialize_with do + { + "type": "Issue", + "check_name": "Rubocop/Metrics/ParameterLists", + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "categories": [ + "Complexity" + ], + "remediation_points": 550000, + "location": { + "path": "file_b.rb", + "positions": { + "begin": { + "column": 14, + "line": 10 + }, + "end": { + "column": 39, + "line": 10 + } + } + }, + "content": { + "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count." + }, + "engine_name": "rubocop", + "fingerprint": "ab5f8b935886b942d621399f5a2ca16e", + "severity": "minor" + }.with_indifferent_access + end + end +end diff --git a/spec/factories/ci/resource.rb b/spec/factories/ci/resource.rb index 515329506e5..dec26013a25 100644 --- a/spec/factories/ci/resource.rb +++ b/spec/factories/ci/resource.rb @@ -5,7 +5,7 @@ FactoryBot.define do resource_group factory: :ci_resource_group trait(:retained) do - build factory: :ci_build + processable factory: :ci_build end end end diff --git a/spec/factories/dependency_proxy.rb b/spec/factories/dependency_proxy.rb index de95df19876..94a7986a8fa 100644 --- a/spec/factories/dependency_proxy.rb +++ b/spec/factories/dependency_proxy.rb @@ -10,7 +10,8 @@ FactoryBot.define do factory :dependency_proxy_manifest, class: 'DependencyProxy::Manifest' do group file { fixture_file_upload('spec/fixtures/dependency_proxy/manifest') } - digest { 'sha256:5ab5a6872b264fe4fd35d63991b9b7d8425f4bc79e7cf4d563c10956581170c9' } + digest { 'sha256:d0710affa17fad5f466a70159cc458227bd25d4afb39514ef662ead3e6c99515' } file_name { 'alpine:latest.json' } + content_type { 'application/vnd.docker.distribution.manifest.v2+json' } end end diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb index 481cabdae6d..f93f3f22109 100644 --- a/spec/factories/merge_request_diffs.rb +++ b/spec/factories/merge_request_diffs.rb @@ -10,12 +10,18 @@ FactoryBot.define do head_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } start_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } + diff_type { :regular } + trait :external do external_diff { fixture_file_upload("spec/fixtures/doc_sample.txt", "plain/txt") } stored_externally { true } importing { true } # this avoids setting the state to 'empty' end + trait :merge_head do + diff_type { :merge_head } + end + factory :external_merge_request_diff, traits: [:external] end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index e69743122cc..dba98593a03 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -200,6 +200,18 @@ FactoryBot.define do end end + trait :with_codequality_mr_diff_reports do + after(:build) do |merge_request| + merge_request.head_pipeline = build( + :ci_pipeline, + :success, + :with_codequality_mr_diff_report, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + end + trait :with_terraform_reports do after(:build) do |merge_request| merge_request.head_pipeline = build( diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb index 31f1aabe5dd..ca38793ac08 100644 --- a/spec/factories/packages.rb +++ b/spec/factories/packages.rb @@ -176,6 +176,24 @@ FactoryBot.define do composer_json { { name: 'foo' } } end + factory :composer_cache_file, class: 'Packages::Composer::CacheFile' do + group + + file_sha256 { '1' * 64 } + + transient do + file_fixture { 'spec/fixtures/packages/composer/package.json' } + end + + after(:build) do |cache_file, evaluator| + cache_file.file = fixture_file_upload(evaluator.file_fixture) + end + + trait(:object_storage) do + file_store { Packages::Composer::CacheUploader::Store::REMOTE } + end + end + factory :maven_metadatum, class: 'Packages::Maven::Metadatum' do association :package, package_type: :maven path { 'my/company/app/my-app/1.0-SNAPSHOT' } diff --git a/spec/factories/packages/debian/group_component.rb b/spec/factories/packages/debian/group_component.rb new file mode 100644 index 00000000000..92d438be389 --- /dev/null +++ b/spec/factories/packages/debian/group_component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :debian_group_component, class: 'Packages::Debian::GroupComponent' do + distribution { association(:debian_group_distribution) } + + sequence(:name) { |n| "group-component-#{n}" } + end +end diff --git a/spec/factories/packages/debian/project_component.rb b/spec/factories/packages/debian/project_component.rb new file mode 100644 index 00000000000..a56aec4cef0 --- /dev/null +++ b/spec/factories/packages/debian/project_component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :debian_project_component, class: 'Packages::Debian::ProjectComponent' do + distribution { association(:debian_project_distribution) } + + sequence(:name) { |n| "project-component-#{n}" } + end +end diff --git a/spec/factories/token_with_ivs.rb b/spec/factories/token_with_ivs.rb new file mode 100644 index 00000000000..68989f6c5bc --- /dev/null +++ b/spec/factories/token_with_ivs.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :token_with_iv do + hashed_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) } + iv { ::Digest::SHA256.digest(SecureRandom.hex(50)) } + hashed_plaintext_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) } + end +end diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb index 7017b0ee9e7..40ad221415c 100644 --- a/spec/factories/u2f_registrations.rb +++ b/spec/factories/u2f_registrations.rb @@ -2,6 +2,8 @@ FactoryBot.define do factory :u2f_registration do + user + certificate { FFaker::BaconIpsum.characters(728) } key_handle { FFaker::BaconIpsum.characters(86) } public_key { FFaker::BaconIpsum.characters(88) } diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb deleted file mode 100644 index 982a9333275..00000000000 --- a/spec/features/admin/admin_cohorts_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Cohorts page' do - before do - admin = create(:admin) - sign_in(admin) - gitlab_enable_admin_mode_sign_in(admin) - end - - context 'with usage ping enabled' do - it 'shows users count per month' do - stub_application_setting(usage_ping_enabled: true) - - create_list(:user, 2) - - visit admin_cohorts_path - - expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") - end - end - - context 'with usage ping disabled' do - it 'shows empty state', :js do - stub_application_setting(usage_ping_enabled: false) - - visit admin_cohorts_path - - expect(page).to have_selector(".js-empty-state") - end - end -end diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index f7f0592a315..b370b779afe 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -37,7 +37,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do it 'shows only the SSH clone information' do resize_screen_xs visit_project - find('.dropdown-toggle').click + + within('.js-mobile-git-clone') do + find('.dropdown-toggle').click + end expect(page).to have_content('Copy SSH clone URL') expect(page).not_to have_content('Copy HTTP clone URL') @@ -66,7 +69,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do it 'shows only the HTTP clone information' do resize_screen_xs visit_project - find('.dropdown-toggle').click + + within('.js-mobile-git-clone') do + find('.dropdown-toggle').click + end expect(page).to have_content('Copy HTTP clone URL') expect(page).not_to have_content('Copy SSH clone URL') @@ -97,7 +103,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do it 'shows both SSH and HTTP clone information' do resize_screen_xs visit_project - find('.dropdown-toggle').click + + within('.js-mobile-git-clone') do + find('.dropdown-toggle').click + end expect(page).to have_content('Copy HTTP clone URL') expect(page).to have_content('Copy SSH clone URL') diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index a8e18385bd2..bbdf2f7f4a9 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -54,7 +54,7 @@ RSpec.describe 'Admin Groups' do click_button "Create group" expect(current_path).to eq admin_group_path(Group.find_by(path: path_component)) - content = page.find('div#content-body') + content = page.find('#content-body') h3_texts = content.all('h3').collect(&:text).join("\n") expect(h3_texts).to match group_name li_texts = content.all('li').collect(&:text).join("\n") diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index ff4e592234b..74e6aac8845 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -10,6 +10,8 @@ RSpec.describe "Admin::Projects" do let(:current_user) { create(:admin) } before do + stub_feature_flags(vue_project_members_list: false) + sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user) end @@ -94,6 +96,7 @@ RSpec.describe "Admin::Projects" do describe 'add admin himself to a project' do before do project.add_maintainer(user) + stub_feature_flags(invite_members_group_modal: false) end it 'adds admin a to a project as developer', :js do diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 0c66775c323..78e8ce91c10 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -315,50 +315,59 @@ RSpec.describe 'Admin updates settings' do end context 'Container Registry' do - context 'delete tags service execution timeout' do - let(:feature_flag_enabled) { true } - let(:client_support) { true } - - before do - stub_container_registry_config(enabled: true) - stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled) - allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support) - end + let(:feature_flag_enabled) { true } + let(:client_support) { true } + let(:settings_titles) do + { + container_registry_delete_tags_service_timeout: 'Container Registry delete tags service execution timeout', + container_registry_expiration_policies_worker_capacity: 'Cleanup policy maximum workers running concurrently', + container_registry_cleanup_tags_service_max_list_size: 'Cleanup policy maximum number of tags to be deleted' + } + end + + before do + stub_container_registry_config(enabled: true) + stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled) + allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support) + end - RSpec.shared_examples 'not having service timeout settings' do - it 'lacks the timeout settings' do - visit ci_cd_admin_application_settings_path + shared_examples 'not having container registry setting' do |registry_setting| + it "lacks the container setting #{registry_setting}" do + visit ci_cd_admin_application_settings_path - expect(page).not_to have_content "Container Registry delete tags service execution timeout" - end + expect(page).not_to have_content(settings_titles[registry_setting]) end + end - context 'with feature flag enabled' do - context 'with client supporting tag delete' do - it 'changes the timeout' do - visit ci_cd_admin_application_settings_path + %i[container_registry_delete_tags_service_timeout container_registry_expiration_policies_worker_capacity container_registry_cleanup_tags_service_max_list_size].each do |setting| + context "for container registry setting #{setting}" do + context 'with feature flag enabled' do + context 'with client supporting tag delete' do + it 'changes the setting' do + visit ci_cd_admin_application_settings_path - page.within('.as-registry') do - fill_in 'application_setting_container_registry_delete_tags_service_timeout', with: 400 - click_button 'Save changes' - end + page.within('.as-registry') do + fill_in "application_setting_#{setting}", with: 400 + click_button 'Save changes' + end - expect(current_settings.container_registry_delete_tags_service_timeout).to eq(400) - expect(page).to have_content "Application settings saved successfully" + expect(current_settings.public_send(setting)).to eq(400) + expect(page).to have_content "Application settings saved successfully" + end end - end - context 'with client not supporting tag delete' do - let(:client_support) { false } + context 'with client not supporting tag delete' do + let(:client_support) { false } - it_behaves_like 'not having service timeout settings' + it_behaves_like 'not having container registry setting', setting + end end - end - context 'with feature flag disabled' do - let(:feature_flag_enabled) { false } + context 'with feature flag disabled' do + let(:feature_flag_enabled) { false } - it_behaves_like 'not having service timeout settings' + it_behaves_like 'not having container registry setting', setting + end end end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb new file mode 100644 index 00000000000..4fc60d17886 --- /dev/null +++ b/spec/features/admin/admin_users_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Admin::Users" do + let(:current_user) { create(:admin) } + + before do + sign_in(current_user) + gitlab_enable_admin_mode_sign_in(current_user) + end + + describe 'Tabs', :js do + let(:tabs_selector) { '.js-users-tabs' } + let(:active_tab_selector) { '.nav-link.active' } + + it 'does not add the tab param when the Users tab is selected' do + visit admin_users_path + + within tabs_selector do + click_link 'Users' + end + + expect(page).to have_current_path(admin_users_path) + end + + it 'adds the ?tab=cohorts param when the Cohorts tab is selected' do + visit admin_users_path + + within tabs_selector do + click_link 'Cohorts' + end + + expect(page).to have_current_path(admin_users_path(tab: 'cohorts')) + end + + it 'shows the cohorts tab when the tab param is set' do + visit admin_users_path(tab: 'cohorts') + + within tabs_selector do + expect(page).to have_selector active_tab_selector, text: 'Cohorts' + end + end + end + + describe 'Cohorts tab content' do + context 'with usage ping enabled' do + it 'shows users count per month' do + stub_application_setting(usage_ping_enabled: true) + + create_list(:user, 2) + + visit admin_users_path(tab: 'cohorts') + + expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") + end + end + + context 'with usage ping disabled' do + it 'shows empty state', :js do + stub_application_setting(usage_ping_enabled: false) + + visit admin_users_path(tab: 'cohorts') + + expect(page).to have_selector(".js-empty-state") + expect(page).to have_content("Activate user activity analysis") + end + end + end +end diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb index d190e4b6939..ce82b5adf8d 100644 --- a/spec/features/alert_management/alert_details_spec.rb +++ b/spec/features/alert_management/alert_details_spec.rb @@ -47,7 +47,7 @@ RSpec.describe 'Alert details', :js do expect(page).to have_selector('[data-testid="alert-todo-button"]') todo_button = find('[data-testid="alert-todo-button"]') - expect(todo_button).to have_content('Add a To-Do') + expect(todo_button).to have_content('Add a to do') find('[data-testid="alert-todo-button"]').click wait_for_requests diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb index 698a36d3f76..07c87f98eb6 100644 --- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb +++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb @@ -19,13 +19,14 @@ RSpec.describe 'Alert integrations settings form', :js do describe 'when viewing alert integrations as a maintainer' do context 'with the default page permissions' do before do + stub_feature_flags(multiple_http_integrations_custom_mapping: false) visit project_settings_operations_path(project, anchor: 'js-alert-management-settings') wait_for_requests end it 'shows the alerts setting form title' do page.within('#js-alert-management-settings') do - expect(find('h3')).to have_content('Alerts') + expect(find('h4')).to have_content('Alerts') end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index b3cc2eb418d..bcc653991c9 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -13,6 +13,8 @@ RSpec.describe 'Issue Boards', :js do let_it_be(:user2) { create(:user) } before do + stub_feature_flags(board_new_list: false) + project.add_maintainer(user) project.add_maintainer(user2) diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 2af5b787a78..cf7554b3646 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -411,10 +411,10 @@ RSpec.describe 'Issue Boards', :js do wait_for_requests page.within('.subscriptions') do - find('.js-issuable-subscribe-button button:not(.is-checked)').click + find('[data-testid="subscription-toggle"] button:not(.is-checked)').click wait_for_requests - expect(page).to have_css('.js-issuable-subscribe-button button.is-checked') + expect(page).to have_css('[data-testid="subscription-toggle"] button.is-checked') end end @@ -427,10 +427,10 @@ RSpec.describe 'Issue Boards', :js do wait_for_requests page.within('.subscriptions') do - find('.js-issuable-subscribe-button button.is-checked').click + find('[data-testid="subscription-toggle"] button.is-checked').click wait_for_requests - expect(page).to have_css('.js-issuable-subscribe-button button:not(.is-checked)') + expect(page).to have_css('[data-testid="subscription-toggle"] button:not(.is-checked)') end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index f8e84043c1b..1622979812d 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -138,9 +138,8 @@ RSpec.describe 'Commits' do end end - context 'when accessing internal project with disallowed access', :js do + context 'when accessing internal project with disallowed access', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299575' do before do - stub_feature_flags(graphql_pipeline_header: false) project.update( visibility_level: Gitlab::VisibilityLevel::INTERNAL, public_builds: false) diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb index 2ad77a2884c..86743e31fbd 100644 --- a/spec/features/discussion_comments/issue_spec.rb +++ b/spec/features/discussion_comments/issue_spec.rb @@ -8,6 +8,8 @@ RSpec.describe 'Thread Comments Issue', :js do let(:issue) { create(:issue, project: project) } before do + stub_feature_flags(remove_comment_close_reopen: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb index 761cc7ae796..82dcdf9f918 100644 --- a/spec/features/discussion_comments/merge_request_spec.rb +++ b/spec/features/discussion_comments/merge_request_spec.rb @@ -9,6 +9,7 @@ RSpec.describe 'Thread Comments Merge Request', :js do before do stub_feature_flags(remove_resolve_note: false) + stub_feature_flags(remove_comment_close_reopen: false) project.add_maintainer(user) sign_in(user) diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb index b2d3fbf4b5d..42053e571e9 100644 --- a/spec/features/discussion_comments/snippets_spec.rb +++ b/spec/features/discussion_comments/snippets_spec.rb @@ -4,15 +4,34 @@ require 'spec_helper' RSpec.describe 'Thread Comments Snippet', :js do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project, author: user) } before do - project.add_maintainer(user) sign_in(user) + end + + context 'with project snippets' do + let_it_be(:project) do + create(:project).tap do |p| + p.add_maintainer(user) + end + end + + let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project, author: user) } + + before do + visit project_snippet_path(project, snippet) + end - visit project_snippet_path(project, snippet) + it_behaves_like 'thread comments', 'snippet' end - it_behaves_like 'thread comments', 'snippet' + context 'with personal snippets' do + let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: user) } + + before do + visit snippet_path(snippet) + end + + it_behaves_like 'thread comments', 'snippet' + end end diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb index 2e1bf27ba8b..73de49101ea 100644 --- a/spec/features/groups/import_export/connect_instance_spec.rb +++ b/spec/features/groups/import_export/connect_instance_spec.rb @@ -23,8 +23,8 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do source_url = 'https://gitlab.com' pat = 'demo-pat' stub_path = 'stub-group' - - stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=30&top_level_only=true&min_access_level=40" % { url: source_url }).to_return( + total = 37 + stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=40&search=" % { url: source_url }).to_return( body: [{ id: 2595438, web_url: 'https://gitlab.com/groups/auto-breakfast', @@ -33,17 +33,25 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do full_name: 'Stub', full_path: stub_path }].to_json, - headers: { 'Content-Type' => 'application/json' } + headers: { + 'Content-Type' => 'application/json', + 'X-Next-Page' => 2, + 'X-Page' => 1, + 'X-Per-Page' => 20, + 'X-Total' => total, + 'X-Total-Pages' => 2 + } ) expect(page).to have_content 'Import groups from another instance of GitLab' + expect(page).to have_content 'Not all related objects are migrated' fill_in :bulk_import_gitlab_url, with: source_url fill_in :bulk_import_gitlab_access_token, with: pat click_on 'Connect instance' - expect(page).to have_content 'Importing groups from %{url}' % { url: source_url } + expect(page).to have_content 'Showing 1-1 of %{total} groups from %{url}' % { url: source_url, total: total } expect(page).to have_content stub_path end end diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb index a4c450c9a2c..7025874a4ff 100644 --- a/spec/features/groups/navbar_spec.rb +++ b/spec/features/groups/navbar_spec.rb @@ -87,12 +87,4 @@ RSpec.describe 'Group navbar' do it_behaves_like 'verified navigation bar' end - - context 'when invite team members is not available' do - it 'does not display the js-invite-members-trigger' do - visit group_path(group) - - expect(page).not_to have_selector('.js-invite-members-trigger') - end - end end diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb index b8ffd73335d..09b0707492f 100644 --- a/spec/features/groups/settings/packages_and_registries_spec.rb +++ b/spec/features/groups/settings/packages_and_registries_spec.rb @@ -47,6 +47,13 @@ RSpec.describe 'Group Packages & Registries settings' do sidebar = find('.nav-sidebar') expect(sidebar).to have_link _('Packages & Registries') end + + it 'has a Package Registry section', :js do + visit_settings_page + + expect(page).to have_content('Package Registry') + expect(page).to have_button('Collapse') + end end def find_settings_menu diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 3a42fd508b4..5067f11be67 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -163,6 +163,7 @@ RSpec.describe 'Group show page' do let!(:project) { create(:project, namespace: group) } before do + stub_feature_flags(vue_notification_dropdown: false) group.add_maintainer(maintainer) sign_in(maintainer) end diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 3f00bdc478d..a0786d36fdf 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -53,7 +53,7 @@ RSpec.describe 'issuable list', :js do visit_issuable_list(:issue) - expect(page).to have_text('Open ? Closed ? All ?') + expect(page).to have_text('Open Closed All') end it "counts merge requests closing issues icons for each issue" do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 9b2a11c4b0e..e2087868035 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -183,6 +183,16 @@ RSpec.describe 'GFM autocomplete', :js do expect(find('#at-view-users')).to have_content(user.name) end + it 'searches across full name for assignees' do + page.within '.timeline-content-form' do + find('#note-body').native.send_keys('@speciąlsome') + end + + wait_for_requests + + expect(find('.atwho-view li', visible: true)).to have_content(user.name) + end + it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do find('#note-body').native.send_keys(':1') diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 59fba5f65e0..ca44978d223 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -11,6 +11,11 @@ RSpec.describe 'Issue Sidebar' do let!(:label) { create(:label, project: project, title: 'bug') } let(:issue) { create(:labeled_issue, project: project, labels: [label]) } let!(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } + let!(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) } + let!(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } + let!(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } + let!(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } + let!(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") @@ -134,6 +139,36 @@ RSpec.describe 'Issue Sidebar' do end end + context 'editing issue milestone', :js do + before do + page.within('.block.milestone > .title') do + click_on 'Edit' + end + end + + it 'shows milestons list in the dropdown' do + page.within('.block.milestone .dropdown-content') do + # 5 milestones + "No milestone" = 6 items + expect(page.find('ul')).to have_selector('li[data-milestone-id]', count: 6) + end + end + + it 'shows expired milestone at the bottom of the list' do + page.within('.block.milestone .dropdown-content ul') do + expect(page.find('li:last-child')).to have_content milestone_expired.title + end + end + + it 'shows milestone due earliest at the top of the list' do + page.within('.block.milestone .dropdown-content ul') do + expect(page.all('li[data-milestone-id]')[1]).to have_content milestone3.title + expect(page.all('li[data-milestone-id]')[2]).to have_content milestone2.title + expect(page.all('li[data-milestone-id]')[3]).to have_content milestone1.title + expect(page.all('li[data-milestone-id]')[4]).to have_content milestone_no_duedate.title + end + end + end + context 'editing issue labels', :js do before do issue.update(labels: [label]) diff --git a/spec/features/issues/issue_state_spec.rb b/spec/features/issues/issue_state_spec.rb index d5a115433aa..409f498798b 100644 --- a/spec/features/issues/issue_state_spec.rb +++ b/spec/features/issues/issue_state_spec.rb @@ -42,9 +42,15 @@ RSpec.describe 'issue state', :js do end describe 'when open', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297348' do - context 'when clicking the top `Close issue` button', :aggregate_failures do - let(:open_issue) { create(:issue, project: project) } + let(:open_issue) { create(:issue, project: project) } + it_behaves_like 'page with comment and close button', 'Close issue' do + def setup + visit project_issue_path(project, open_issue) + end + end + + context 'when clicking the top `Close issue` button', :aggregate_failures do before do visit project_issue_path(project, open_issue) end @@ -53,9 +59,8 @@ RSpec.describe 'issue state', :js do end context 'when clicking the bottom `Close issue` button', :aggregate_failures do - let(:open_issue) { create(:issue, project: project) } - before do + stub_feature_flags(remove_comment_close_reopen: false) visit project_issue_path(project, open_issue) end @@ -64,9 +69,15 @@ RSpec.describe 'issue state', :js do end describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297201' do - context 'when clicking the top `Reopen issue` button', :aggregate_failures do - let(:closed_issue) { create(:issue, project: project, state: 'closed') } + let(:closed_issue) { create(:issue, project: project, state: 'closed') } + it_behaves_like 'page with comment and close button', 'Reopen issue' do + def setup + visit project_issue_path(project, closed_issue) + end + end + + context 'when clicking the top `Reopen issue` button', :aggregate_failures do before do visit project_issue_path(project, closed_issue) end @@ -75,9 +86,8 @@ RSpec.describe 'issue state', :js do end context 'when clicking the bottom `Reopen issue` button', :aggregate_failures do - let(:closed_issue) { create(:issue, project: project, state: 'closed') } - before do + stub_feature_flags(remove_comment_close_reopen: false) visit project_issue_path(project, closed_issue) end diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index 971c8a3b431..d91c187c840 100644 --- a/spec/features/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb @@ -15,13 +15,13 @@ RSpec.describe "User toggles subscription", :js do end it "unsubscribes from issue" do - subscription_button = find(".js-issuable-subscribe-button") + subscription_button = find('[data-testid="subscription-toggle"]') # Check we're subscribed. expect(subscription_button).to have_css("button.is-checked") # Toggle subscription. - find(".js-issuable-subscribe-button button").click + find('[data-testid="subscription-toggle"]').click wait_for_requests # Check we're unsubscribed. @@ -33,7 +33,7 @@ RSpec.describe "User toggles subscription", :js do it 'is disabled' do expect(page).to have_content('Notifications have been disabled by the project or group owner') - expect(page).not_to have_selector('.js-issuable-subscribe-button') + expect(page).not_to have_selector('[data-testid="subscription-toggle"]') end end end diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index 5d141580874..aeb42cc2edb 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -18,6 +18,7 @@ RSpec.describe 'Labels Hierarchy', :js do before do stub_feature_flags(graphql_board_lists: false) + stub_feature_flags(board_new_list: false) grandparent.add_owner(user) sign_in(user) @@ -270,6 +271,10 @@ RSpec.describe 'Labels Hierarchy', :js do end context 'creating boards lists' do + before do + stub_feature_flags(board_new_list: false) + end + context 'on project boards' do let(:board) { create(:board, project: project_1) } diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb index 23cdd9d2ce5..207678e07c3 100644 --- a/spec/features/markdown/mermaid_spec.rb +++ b/spec/features/markdown/mermaid_spec.rb @@ -108,7 +108,7 @@ RSpec.describe 'Mermaid rendering', :js do expect(svg[:style]).to match(/max-width/) expect(svg[:width].to_i).to eq(100) - expect(svg[:height].to_i).to eq(0) + expect(svg[:height].to_i).to be_within(5).of(220) end end 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 2b94c072c8b..ab3ef7c1ac0 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 @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'User closes/reopens a merge request', :js do +RSpec.describe 'User closes/reopens a merge request', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -12,9 +12,15 @@ RSpec.describe 'User closes/reopens a merge request', :js do end describe 'when open' do - context 'when clicking the top `Close merge request` link', :aggregate_failures do - let(:open_merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:open_merge_request) { create(:merge_request, source_project: project, target_project: project) } + + it_behaves_like 'page with comment and close button', 'Close merge request' do + def setup + visit merge_request_path(open_merge_request) + end + end + context 'when clicking the top `Close merge request` link', :aggregate_failures do before do visit merge_request_path(open_merge_request) end @@ -34,9 +40,8 @@ RSpec.describe 'User closes/reopens a merge request', :js do end context 'when clicking the bottom `Close merge request` button', :aggregate_failures do - let(:open_merge_request) { create(:merge_request, source_project: project, target_project: project) } - before do + stub_feature_flags(remove_comment_close_reopen: false) visit merge_request_path(open_merge_request) end @@ -55,10 +60,23 @@ RSpec.describe 'User closes/reopens a merge request', :js do end end - describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do - context 'when clicking the top `Reopen merge request` link', :aggregate_failures do - let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') } + describe 'when closed' do + let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') } + it_behaves_like 'page with comment and close button', 'Close merge request' do + def setup + visit merge_request_path(closed_merge_request) + + within '.detail-page-header' do + click_button 'Toggle dropdown' + click_link 'Reopen merge request' + end + + wait_for_requests + end + end + + context 'when clicking the top `Reopen merge request` link', :aggregate_failures do before do visit merge_request_path(closed_merge_request) end @@ -78,9 +96,8 @@ RSpec.describe 'User closes/reopens a merge request', :js do end context 'when clicking the bottom `Reopen merge request` button', :aggregate_failures do - let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') } - before do + stub_feature_flags(remove_comment_close_reopen: false) visit merge_request_path(closed_merge_request) end diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb index 9ed5b67fa0e..3cdb22000f6 100644 --- a/spec/features/merge_request/user_manages_subscription_spec.rb +++ b/spec/features/merge_request/user_manages_subscription_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'User manages subscription', :js do end it 'toggles subscription' do - page.within('.js-issuable-subscribe-button') do + page.within('[data-testid="subscription-toggle"]') do wait_for_requests expect(page).to have_css 'button:not(.is-checked)' 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 ea3e90a4508..05873d8655e 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 @@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do wait_for_requests expect(page).to have_css('button[disabled="disabled"]', text: 'Merge') - expect(page).to have_content('Please retry the job or push a new commit to fix the failure') + expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the troubleshooting documentation to see other possible actions.') end end @@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do wait_for_requests expect(page).not_to have_button 'Merge' - expect(page).to have_content('Please retry the job or push a new commit to fix the failure') + expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the troubleshooting documentation to see other possible actions.') end end 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 5e99383e4a1..63b463a2c5f 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 @@ -68,7 +68,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do wait_for_requests - expect(page).to have_content 'Merge when pipeline succeeds', wait: 0 + expect(page).to have_content 'Merge when pipeline succeeds' end it_behaves_like 'Merge when pipeline succeeds activator' @@ -145,7 +145,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do before do merge_request.update!( merge_user: merge_request.author, - merge_error: 'Something went wrong.' + merge_error: 'Something went wrong' ) refresh end @@ -155,7 +155,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong. Please try again.') + expect(page).to have_content('Something went wrong. Try again.') end end end @@ -174,7 +174,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong. Please try again.') + expect(page).to have_content('Something went wrong. Try again.') end end end diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index 489582521b5..e629bc0dc53 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -161,7 +161,7 @@ RSpec.describe 'Merge request > User posts notes', :js do fill_in 'note[note]', with: 'Some new content' accept_confirm do - find('.btn-cancel').click + find('[data-testid="cancel"]').click end end expect(find('.js-note-text').text).to eq '' 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 5e9611de460..d40c9f0ce6f 100644 --- a/spec/features/merge_request/user_reverts_merge_request_spec.rb +++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb @@ -17,46 +17,28 @@ RSpec.describe 'User reverts a merge request', :js do wait_for_requests - visit(merge_request_path(merge_request)) + # do not reload the page by visiting, let javascript update the page as it will validate we have loaded the modal + # code correctly on page update that adds the `revert` button end it 'reverts a merge request', :sidekiq_might_not_need_inline do - find("a[href='#modal-revert-commit']").click + revert_commit - page.within('#modal-revert-commit') do - uncheck('create_merge_request') - click_button('Revert') - end + wait_for_requests expect(page).to have_content('The merge request has been successfully reverted.') - - wait_for_requests end it 'does not revert a merge request that was previously reverted', :sidekiq_might_not_need_inline do - find("a[href='#modal-revert-commit']").click - - page.within('#modal-revert-commit') do - uncheck('create_merge_request') - click_button('Revert') - end - - find("a[href='#modal-revert-commit']").click + revert_commit - page.within('#modal-revert-commit') do - uncheck('create_merge_request') - click_button('Revert') - end + revert_commit expect(page).to have_content('Sorry, we cannot revert this merge request automatically.') end it 'reverts a merge request in a new merge request', :sidekiq_might_not_need_inline do - find("a[href='#modal-revert-commit']").click - - page.within('#modal-revert-commit') do - click_button('Revert') - end + revert_commit(create_merge_request: true) expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') end @@ -68,4 +50,13 @@ RSpec.describe 'User reverts a merge request', :js do expect(page).not_to have_link('Revert') end + + def revert_commit(create_merge_request: false) + click_button('Revert') + + page.within('[data-testid="modal-commit"]') do + uncheck('create_merge_request') unless create_merge_request + click_button('Revert') + end + end end 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 d9743f6f330..708ce53b4fe 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 @@ -160,7 +160,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', it 'merges the merge request' do expect(page).to have_content('Merged by') - expect(page).to have_link('Revert') + expect(page).to have_button('Revert') end end @@ -357,7 +357,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', it 'merges the merge request' do expect(page).to have_content('Merged by') - expect(page).to have_link('Revert') + expect(page).to have_button('Revert') end 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 c2b2ada47be..9bd6cd8863f 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -319,7 +319,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong') + expect(page).to have_content('Something went wrong.') end end end @@ -340,7 +340,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong') + expect(page).to have_content('Something went wrong.') end end end diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index 04d8c52df61..1ef6d2a1068 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -9,152 +9,166 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) } let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test') } - before do - build.run - build.trace.set('hello') - sign_in(user) - visit_merge_request - end - - def visit_merge_request(format: :html, serializer: nil) - visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) - end - - it 'displays a mini pipeline graph' do - expect(page).to have_selector('.mr-widget-pipeline-graph') - end - - context 'as json' do - let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } - let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } - + shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled| before do - job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) - create(:ci_job_artifact, :archive, file: artifacts_file1, job: job) - create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + build.run + build.trace.set('hello') + sign_in(user) + stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled) + visit_merge_request end - # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034 - xit 'avoids repeated database queries' do - before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } - - job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) - create(:ci_job_artifact, :archive, file: artifacts_file2, job: job) - create(:ci_build, :manual, pipeline: pipeline, when: 'manual') - - after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } + let_it_be(:dropdown_toggle_selector) do + if ci_mini_pipeline_gl_dropdown_enabled + '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle' + else + '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + end + end - expect(before.count).to eq(after.count) - expect(before.cached_count).to eq(after.cached_count) + def visit_merge_request(format: :html, serializer: nil) + visit project_merge_request_path(project, merge_request, format: format, serializer: serializer) end - end - describe 'build list toggle' do - let(:toggle) do - find('.mini-pipeline-graph-dropdown-toggle') - first('.mini-pipeline-graph-dropdown-toggle') + it 'displays a mini pipeline graph' do + expect(page).to have_selector('.mr-widget-pipeline-graph') end - # Status icon button styles should update as described in - # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769 - it 'has unique styles for default, :hover, :active, and :focus states' do - find('.mini-pipeline-graph-dropdown-toggle') - default_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") - default_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") - default_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + context 'as json' do + let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } - toggle.hover + before do + job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) + create(:ci_job_artifact, :archive, file: artifacts_file1, job: job) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') + end - find('.mini-pipeline-graph-dropdown-toggle') - hover_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") - hover_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") - hover_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034 + xit 'avoids repeated database queries' do + before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } - page.driver.browser.action.click_and_hold(toggle.native).perform + job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline) + create(:ci_job_artifact, :archive, file: artifacts_file2, job: job) + create(:ci_build, :manual, pipeline: pipeline, when: 'manual') - find('.mini-pipeline-graph-dropdown-toggle') - active_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") - active_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") - active_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') } - page.driver.browser.action.release(toggle.native) - .move_by(100, 100) - .perform + expect(before.count).to eq(after.count) + expect(before.cached_count).to eq(after.cached_count) + end + end - find('.mini-pipeline-graph-dropdown-toggle') - focus_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") - focus_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") - focus_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + describe 'build list toggle' do + let(:toggle) do + find(dropdown_toggle_selector) + first(dropdown_toggle_selector) + end - expect(default_background_color).not_to eq(hover_background_color) - expect(hover_background_color).not_to eq(active_background_color) - expect(default_background_color).not_to eq(active_background_color) + # Status icon button styles should update as described in + # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769 + it 'has unique styles for default, :hover, :active, and :focus states' do + default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector) - expect(default_foreground_color).not_to eq(hover_foreground_color) - expect(hover_foreground_color).not_to eq(active_foreground_color) - expect(default_foreground_color).not_to eq(active_foreground_color) + toggle.hover + hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector) - expect(focus_background_color).to eq(hover_background_color) - expect(focus_foreground_color).to eq(hover_foreground_color) + page.driver.browser.action.click_and_hold(toggle.native).perform + active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector) + page.driver.browser.action.release(toggle.native).perform - expect(default_box_shadow).to eq('none') - expect(hover_box_shadow).to eq('none') - expect(active_box_shadow).not_to eq('none') - expect(focus_box_shadow).not_to eq('none') - end + page.driver.browser.action.click(toggle.native).move_by(100, 100).perform + focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector) - it 'shows tooltip when hovered' do - toggle.hover + expect(default_background_color).not_to eq(hover_background_color) + expect(hover_background_color).not_to eq(active_background_color) + expect(default_background_color).not_to eq(active_background_color) - expect(page).to have_selector('.tooltip') - end - end + expect(default_foreground_color).not_to eq(hover_foreground_color) + expect(hover_foreground_color).not_to eq(active_foreground_color) + expect(default_foreground_color).not_to eq(active_foreground_color) - describe 'builds list menu' do - let(:toggle) do - find('.mini-pipeline-graph-dropdown-toggle') - first('.mini-pipeline-graph-dropdown-toggle') - end + expect(focus_background_color).to eq(hover_background_color) + expect(focus_foreground_color).to eq(hover_foreground_color) - before do - toggle.click - wait_for_requests - end + expect(default_box_shadow).to eq('none') + expect(hover_box_shadow).to eq('none') + expect(active_box_shadow).not_to eq('none') + expect(focus_box_shadow).not_to eq('none') + end + + it 'shows tooltip when hovered' do + toggle.hover - it 'pens when toggle is clicked' do - expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') + expect(page).to have_selector('.tooltip') + end end - it 'closes when toggle is clicked again' do - toggle.click + describe 'builds list menu' do + let(:toggle) do + find(dropdown_toggle_selector) + first(dropdown_toggle_selector) + end - expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') - end + before do + toggle.click + wait_for_requests + end - it 'closes when clicking somewhere else' do - find('body').click + it 'pens when toggle is clicked' do + expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') + end - expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') - end + it 'closes when toggle is clicked again' do + toggle.click - describe 'build list build item' do - let(:build_item) do - find('.mini-pipeline-graph-dropdown-item') - first('.mini-pipeline-graph-dropdown-item') + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end - it 'visits the build page when clicked' do - build_item.click - find('.build-page') + it 'closes when clicking somewhere else' do + find('body').click - expect(current_path).to eql(project_job_path(project, build)) + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end - it 'shows tooltip when hovered' do - build_item.hover + describe 'build list build item' do + let(:build_item) do + find('.mini-pipeline-graph-dropdown-item') + first('.mini-pipeline-graph-dropdown-item') + end - expect(page).to have_selector('.tooltip') + it 'visits the build page when clicked' do + build_item.click + find('.build-page') + + expect(current_path).to eql(project_job_path(project, build)) + end + + it 'shows tooltip when hovered' do + build_item.hover + + expect(page).to have_selector('.tooltip') + end end end end + + context 'with ci_mini_pipeline_gl_dropdown disabled' do + it_behaves_like "mini pipeline renders", false + end + + context 'with ci_mini_pipeline_gl_dropdown enabled' do + it_behaves_like "mini pipeline renders", true + end + + private + + def get_toggle_colors(selector) + find(selector) + [ + evaluate_script("$('#{selector}:visible').css('background-color');"), + evaluate_script("$('#{selector}:visible svg').css('fill');"), + evaluate_script("$('#{selector}:visible').css('box-shadow');") + ] + end end 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 a2ec34335ec..bbeb91bbd19 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 @@ -73,6 +73,23 @@ RSpec.describe 'User comments on a diff', :js do end end + it 'allows suggestions in replies' do + click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion\n# change to a comment\n```") + click_button('Add comment now') + end + + wait_for_requests + + click_button 'Reply...' + + find('.js-suggestion-btn').click + + expect(find('.js-vue-issue-note-form').value).to include("url = https://github.com/gitlabhq/gitlab-shell.git") + end + it 'suggestion is appliable' do click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) 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 e8998f9457a..fdf29d32836 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 @@ -107,5 +107,21 @@ RSpec.describe 'User views an open merge request' do end end end + + context 'when the assignee\'s availability set' do + before do + merge_request.author.create_status(availability: 'busy') + merge_request.assignees << merge_request.author + + visit(merge_request_path(merge_request)) + end + + it 'exposes the availability in the data-availability attribute' do + assignees_data = find_all("input[name='merge_request[assignee_ids][]']", visible: false) + + expect(assignees_data.size).to eq(1) + expect(assignees_data.first['data-availability']).to eq('busy') + end + end end end diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb index 997cc8e3c4b..289fbff0404 100644 --- a/spec/features/profiles/user_visits_notifications_tab_spec.rb +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -7,6 +7,7 @@ RSpec.describe 'User visits the notifications tab', :js do let(:user) { create(:user) } before do + stub_feature_flags(vue_notification_dropdown: false) project.add_maintainer(user) sign_in(user) visit(profile_notifications_path) diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 9fe3f4cd63e..489a90cc8fc 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -2,108 +2,126 @@ require 'spec_helper' -RSpec.describe 'Cherry-pick Commits' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, :repository, namespace: group) } - let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } - let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') } +RSpec.describe 'Cherry-pick Commits', :js do + let_it_be(:user) { create(:user) } + let_it_be(:sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + let!(:project) { create_default(:project, :repository, namespace: user.namespace) } + let(:master_pickable_commit) { project.commit(sha) } before do sign_in(user) - project.add_maintainer(user) - visit project_commit_path(project, master_pickable_commit.id) end - context "I cherry-pick a commit" do - it do - find("a[href='#modal-cherry-pick-commit']").click - expect(page).not_to have_content('v1.0.0') # Only branches, not tags - page.within('#modal-cherry-pick-commit') do - uncheck 'create_merge_request' - click_button 'Cherry-pick' - end - expect(page).to have_content('The commit has been successfully cherry-picked into master.') - end - end + context 'when clicking cherry-pick from the dropdown for a commit on pipelines tab' do + it 'launches the modal form' do + create(:ci_empty_pipeline, sha: sha) + visit project_commit_path(project, master_pickable_commit.id) + click_link 'Pipelines' - context "I cherry-pick a merge commit" do - it do - find("a[href='#modal-cherry-pick-commit']").click - page.within('#modal-cherry-pick-commit') do - uncheck 'create_merge_request' - click_button 'Cherry-pick' + open_modal + + page.within(modal_selector) do + expect(page).to have_content('Cherry-pick this commit') end - expect(page).to have_content('The commit has been successfully cherry-picked into master.') end end - context "I cherry-pick a commit that was previously cherry-picked" do - it do - find("a[href='#modal-cherry-pick-commit']").click - page.within('#modal-cherry-pick-commit') do - uncheck 'create_merge_request' - click_button 'Cherry-pick' - end + context 'when starting from the commit tab' do + before do visit project_commit_path(project, master_pickable_commit.id) - find("a[href='#modal-cherry-pick-commit']").click - page.within('#modal-cherry-pick-commit') do - uncheck 'create_merge_request' - click_button 'Cherry-pick' - end - expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.') end - end - context "I cherry-pick a commit in a new merge request", :js do - it do - find('.header-action-buttons a.dropdown-toggle').click - find("a[href='#modal-cherry-pick-commit']").click - page.within('#modal-cherry-pick-commit') do - click_button 'Cherry-pick' + context 'when cherry-picking a commit' do + specify do + cherry_pick_commit + + expect(page).to have_content('The commit has been successfully cherry-picked into master.') end + end - wait_for_requests + context 'when cherry-picking a merge commit' do + specify do + cherry_pick_commit - expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.") - expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master") + expect(page).to have_content('The commit has been successfully cherry-picked into master.') + end end - end - context "I cherry-pick a commit from a different branch", :js do - it do - find('.header-action-buttons a.dropdown-toggle').click - find(:css, "a[href='#modal-cherry-pick-commit']").click + context 'when cherry-picking a commit that was previously cherry-picked' do + specify do + cherry_pick_commit - page.within('#modal-cherry-pick-commit') do - click_button 'master' + visit project_commit_path(project, master_pickable_commit.id) + + cherry_pick_commit + + expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.') end + end - wait_for_requests + context 'when cherry-picking a commit in a new merge request' do + specify do + cherry_pick_commit(create_merge_request: true) - page.within('#modal-cherry-pick-commit .dropdown-menu') do - find('.dropdown-input input').set('feature') - wait_for_requests - click_link "feature" + expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.") + expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master") end + end - page.within('#modal-cherry-pick-commit') do - uncheck 'create_merge_request' - click_button 'Cherry-pick' + context 'when I cherry-picking a commit from a different branch' do + specify do + open_modal + + page.within(modal_selector) do + click_button 'master' + end + + page.within("#{modal_selector} .dropdown-menu") do + find('[data-testid="dropdown-search-box"]').set('feature') + wait_for_requests + click_button 'feature' + end + + submit_cherry_pick + + expect(page).to have_content('The commit has been successfully cherry-picked into feature.') end + end + + context 'when the project is archived' do + let(:project) { create(:project, :repository, :archived, namespace: user.namespace) } - expect(page).to have_content('The commit has been successfully cherry-picked into feature.') + it 'does not show the cherry-pick link' do + open_dropdown + + expect(page).not_to have_text("Cherry-pick") + end end end - context 'when the project is archived' do - let(:project) { create(:project, :repository, :archived, namespace: group) } + def cherry_pick_commit(create_merge_request: false) + open_modal - it 'does not show the cherry-pick link' do - find('.header-action-buttons a.dropdown-toggle').click + submit_cherry_pick(create_merge_request: create_merge_request) + end + + def open_dropdown + find('.header-action-buttons .dropdown').click + end - expect(page).not_to have_text("Cherry-pick") - expect(page).not_to have_css("a[href='#modal-cherry-pick-commit']") + def open_modal + open_dropdown + find('[data-testid="cherry-pick-commit-link"]').click + end + + def submit_cherry_pick(create_merge_request: false) + page.within(modal_selector) do + uncheck('create_merge_request') unless create_merge_request + click_button('Cherry-pick') end end + + def modal_selector + '[data-testid="modal-commit"]' + end end diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb index f3c364dab97..72c639a027e 100644 --- a/spec/features/projects/commit/user_reverts_commit_spec.rb +++ b/spec/features/projects/commit/user_reverts_commit_spec.rb @@ -6,58 +6,89 @@ RSpec.describe 'User reverts a commit', :js do include RepoHelpers let_it_be(:user) { create(:user) } - let(:project) { create(:project, :repository, namespace: user.namespace) } + let!(:project) { create_default(:project, :repository, namespace: user.namespace) } before do sign_in(user) - - visit(project_commit_path(project, sample_commit.id)) end - def revert_commit(create_merge_request: false) - find('.header-action-buttons .dropdown').click - find('[data-testid="revert-commit-link"]').click + context 'when clicking revert from the dropdown for a commit on pipelines tab' do + it 'launches the modal and is able to submit the revert' do + sha = '7d3b0f7cff5f37573aea97cebfd5692ea1689924' + create(:ci_empty_pipeline, sha: sha) + visit project_commit_path(project, project.commit(sha).id) + click_link 'Pipelines' - page.within('[data-testid="modal-commit"]') do - uncheck('create_merge_request') unless create_merge_request - click_button('Revert') + open_modal + + page.within(modal_selector) do + expect(page).to have_content('Revert this commit') + end end end - context 'without creating a new merge request' do - it 'reverts a commit' do - revert_commit + context 'when starting from the commit tab' do + before do + visit project_commit_path(project, sample_commit.id) + end + + context 'without creating a new merge request' do + it 'reverts a commit' do + revert_commit + + expect(page).to have_content('The commit has been successfully reverted.') + end + + it 'does not revert a previously reverted commit' do + revert_commit + # Visit the comment again once it was reverted. + visit project_commit_path(project, sample_commit.id) + + revert_commit - expect(page).to have_content('The commit has been successfully reverted.') + expect(page).to have_content('Sorry, we cannot revert this commit automatically.') + end end - it 'does not revert a previously reverted commit' do - revert_commit - # Visit the comment again once it was reverted. - visit project_commit_path(project, sample_commit.id) + context 'with creating a new merge request' do + it 'reverts a commit' do + revert_commit(create_merge_request: true) + + expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") + end + end - revert_commit + context 'when the project is archived' do + let(:project) { create(:project, :repository, :archived, namespace: user.namespace) } - expect(page).to have_content('Sorry, we cannot revert this commit automatically.') + it 'does not show the revert link' do + open_dropdown + + expect(page).not_to have_link('Revert') + end end end - context 'with creating a new merge request' do - it 'reverts a commit' do - revert_commit(create_merge_request: true) + def revert_commit(create_merge_request: false) + open_modal - expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') - expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") + page.within(modal_selector) do + uncheck('create_merge_request') unless create_merge_request + click_button('Revert') end end - context 'when the project is archived' do - let(:project) { create(:project, :repository, :archived, namespace: user.namespace) } + def open_dropdown + find('.header-action-buttons .dropdown').click + end - it 'does not show the revert link' do - find('.header-action-buttons .dropdown').click + def open_modal + open_dropdown + find('[data-testid="revert-commit-link"]').click + end - expect(page).not_to have_link('Revert') - end + def modal_selector + '[data-testid="modal-commit"]' end end diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 596b4773716..4894e2b7f3e 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -203,10 +203,11 @@ RSpec.describe 'User browses commits' do context 'when click the compare tab' do before do + wait_for_requests click_link('Compare') end - it 'does not render create merge request button' do + it 'does not render create merge request button', :js do expect(page).not_to have_link 'Create merge request' end end @@ -236,10 +237,11 @@ RSpec.describe 'User browses commits' do context 'when click the compare tab' do before do + wait_for_requests click_link('Compare') end - it 'renders create merge request button' do + it 'renders create merge request button', :js do expect(page).to have_link 'Create merge request' end end @@ -276,10 +278,11 @@ RSpec.describe 'User browses commits' do context 'when click the compare tab' do before do + wait_for_requests click_link('Compare') end - it 'renders button to the merge request' do + it 'renders button to the merge request', :js do expect(page).not_to have_link 'Create merge request' expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request) end diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index e387ea4d473..64e9968061c 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -17,10 +17,10 @@ RSpec.describe "Compare", :js do visit project_compare_index_path(project, from: 'master', to: 'master') select_using_dropdown 'from', 'feature' - expect(find('.js-compare-from-dropdown .dropdown-toggle-text')).to have_content('feature') + expect(find('.js-compare-from-dropdown .gl-new-dropdown-button-text')).to have_content('feature') select_using_dropdown 'to', 'binary-encoding' - expect(find('.js-compare-to-dropdown .dropdown-toggle-text')).to have_content('binary-encoding') + expect(find('.js-compare-to-dropdown .gl-new-dropdown-button-text')).to have_content('binary-encoding') click_button 'Compare' @@ -32,8 +32,8 @@ RSpec.describe "Compare", :js do it "pre-populates fields" do visit project_compare_index_path(project, from: "master", to: "master") - expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master") - expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master") + expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("master") + expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("master") end it_behaves_like 'compares branches' @@ -99,7 +99,7 @@ RSpec.describe "Compare", :js do find(".js-compare-from-dropdown .compare-dropdown-toggle").click - expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3) + expect(find(".js-compare-from-dropdown .gl-new-dropdown-contents")).to have_selector('li.gl-new-dropdown-item', count: 1) end context 'when commit has overflow', :js do @@ -125,10 +125,10 @@ RSpec.describe "Compare", :js do visit project_compare_index_path(project, from: "master", to: "master") select_using_dropdown "from", "v1.0.0" - expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0") + expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("v1.0.0") select_using_dropdown "to", "v1.1.0" - expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0") + expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("v1.1.0") click_button "Compare" expect(page).to have_content "Commits" @@ -136,19 +136,22 @@ RSpec.describe "Compare", :js do end def select_using_dropdown(dropdown_type, selection, commit: false) + wait_for_requests + dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown.find(".compare-dropdown-toggle").click # find input before using to wait for the inputs visibility dropdown.find('.dropdown-menu') dropdown.fill_in("Filter by Git revision", with: selection) + wait_for_requests if commit - dropdown.find('input[type="search"]').send_keys(:return) + dropdown.find('.gl-search-box-by-type-input').send_keys(:return) else # find before all to wait for the items visibility - dropdown.find("a[data-ref=\"#{selection}\"]", match: :first) - dropdown.all("a[data-ref=\"#{selection}\"]").last.click + dropdown.find(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection, match: :first) + dropdown.all(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection).first.click end end end diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb index 17258f7042f..40d19a94b42 100644 --- a/spec/features/projects/files/dockerfile_dropdown_spec.rb +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -2,14 +2,16 @@ require 'spec_helper' -RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297400' do +RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js do + include Spec::Support::Helpers::Features::EditorLiteSpecHelpers + before do project = create(:project, :repository) sign_in project.owner visit project_new_blob_path(project, 'master', file_name: 'Dockerfile') end - it 'user can pick a Dockerfile file from the dropdown', :js do + it 'user can pick a Dockerfile file from the dropdown' do expect(page).to have_css('.dockerfile-selector') find('.js-dockerfile-selector').click @@ -24,6 +26,6 @@ RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', quarant wait_for_requests expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'Apply a template') - expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/') + expect(editor_get_value).to have_content('COPY ./ /usr/local/apache2/htdocs/') end end diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb index 5a39f2bcd98..a9f2463ecf6 100644 --- a/spec/features/projects/files/gitignore_dropdown_spec.rb +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -2,14 +2,16 @@ require 'spec_helper' -RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do +RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js do + include Spec::Support::Helpers::Features::EditorLiteSpecHelpers + before do project = create(:project, :repository) sign_in project.owner visit project_new_blob_path(project, 'master', file_name: '.gitignore') end - it 'user can pick a .gitignore file from the dropdown', :js do + it 'user can pick a .gitignore file from the dropdown' do expect(page).to have_css('.gitignore-selector') find('.js-gitignore-selector').click @@ -24,7 +26,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do wait_for_requests expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template') - expect(page).to have_content('/.bundle') - expect(page).to have_content('config/initializers/secret_token.rb') + expect(editor_get_value).to have_content('/.bundle') + expect(editor_get_value).to have_content('config/initializers/secret_token.rb') end end diff --git a/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb index 6308acb41f5..ca6f03472dd 100644 --- a/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb +++ b/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do + include Spec::Support::Helpers::Features::EditorLiteSpecHelpers + before do project = create(:project, :repository) sign_in project.owner @@ -34,8 +36,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do let(:experiment_active) { true } let(:in_experiment_group) { true } - it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js, - { quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297347' } } do + it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js do expect(page).to have_css('.gitlab-ci-syntax-yml-selector') find('.js-gitlab-ci-syntax-yml-selector').click @@ -50,7 +51,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do wait_for_requests expect(page).to have_css('.gitlab-ci-syntax-yml-selector .dropdown-toggle-text', text: 'Learn CI/CD syntax') - expect(page).to have_content('You can use artifacts to pass data to jobs in later stages.') + expect(editor_get_value).to have_content('You can use artifacts to pass data to jobs in later stages.') end end end diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb index 879cb6a65c8..55b9f38d8e7 100644 --- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -2,14 +2,16 @@ require 'spec_helper' -RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do +RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js do + include Spec::Support::Helpers::Features::EditorLiteSpecHelpers + before do project = create(:project, :repository) sign_in project.owner visit project_new_blob_path(project, 'master', file_name: '.gitlab-ci.yml') end - it 'user can pick a template from the dropdown', :js do + it 'user can pick a template from the dropdown' do expect(page).to have_css('.gitlab-ci-yml-selector') find('.js-gitlab-ci-yml-selector').click @@ -24,7 +26,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do wait_for_requests expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template') - expect(page).to have_content('This file is a template, and might need editing before it works on your project') - expect(page).to have_content('jekyll build -d test') + expect(editor_get_value).to have_content('This file is a template, and might need editing before it works on your project') + expect(editor_get_value).to have_content('jekyll build -d test') end end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 1557a8a2d72..adf664f26af 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -533,10 +533,10 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).to have_content('Trigger token') expect(page).to have_content('Trigger variables') - expect(page).not_to have_css('.js-reveal-variables') + expect(page).not_to have_selector('[data-testid="trigger-reveal-values-button"]') - expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') - expect(page).to have_selector('.js-build-value', text: '••••••') + expect(page).to have_selector('[data-testid="trigger-build-key"]', text: 'TRIGGER_KEY_1') + expect(page).to have_selector('[data-testid="trigger-build-value"]', text: '••••••') end end @@ -571,17 +571,17 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).to have_content('Trigger token') expect(page).to have_content('Trigger variables') - expect(page).to have_css('.js-reveal-variables') + expect(page).to have_selector('[data-testid="trigger-reveal-values-button"]') - expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') - expect(page).to have_selector('.js-build-value', text: '••••••') + expect(page).to have_selector('[data-testid="trigger-build-key"]', text: 'TRIGGER_KEY_1') + expect(page).to have_selector('[data-testid="trigger-build-value"]', text: '••••••') end it 'reveals values on button click', :js do click_button 'Reveal values' - expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') - expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') + expect(page).to have_selector('[data-testid="trigger-build-key"]', text: 'TRIGGER_KEY_1') + expect(page).to have_selector('[data-testid="trigger-build-value"]', text: 'TRIGGER_VALUE_1') end end diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb index 3b0f00c5494..c0849cc7330 100644 --- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb +++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb @@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Anonymous user sees members' do let(:project) { create(:project, :public) } before do + stub_feature_flags(vue_project_members_list: false) + project.add_maintainer(user) create(:project_group_link, project: project, group: group) end diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index aa15f04bf24..b6a5fbf5584 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -13,6 +13,8 @@ RSpec.describe 'Projects members', :js do let(:group_requester) { create(:user) } before do + stub_feature_flags(vue_project_members_list: false) + project.add_developer(developer) group.add_owner(user) sign_in(user) diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb index 686d86b1783..de27692b535 100644 --- a/spec/features/projects/members/groups_with_access_list_spec.rb +++ b/spec/features/projects/members/groups_with_access_list_spec.rb @@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do let!(:group_link) { create(:project_group_link, project: project, group: group, **additional_link_attrs) } before do + stub_feature_flags(vue_project_members_list: false) + travel_to Time.now.utc.beginning_of_day project.add_maintainer(user) diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index bb56ae348fb..8e956c4a7cd 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -8,6 +8,11 @@ RSpec.describe 'Project > Members > Invite group', :js do let(:maintainer) { create(:user) } + before do + stub_feature_flags(invite_members_group_modal: false) + stub_feature_flags(vue_project_members_list: false) + end + describe 'Share with group lock' do shared_examples 'the project can be shared with groups' do it 'the "Invite group" tab exists' do diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb index 62115f2dce6..a62ccfab244 100644 --- a/spec/features/projects/members/list_spec.rb +++ b/spec/features/projects/members/list_spec.rb @@ -13,10 +13,18 @@ RSpec.describe 'Project members list' do before do stub_feature_flags(invite_members_group_modal: false) + stub_feature_flags(vue_project_members_list: false) + sign_in(user1) group.add_owner(user1) end + it 'pushes `vue_project_members_list` feature flag to the frontend' do + visit_members_page + + expect(page).to have_pushed_frontend_feature_flags(vueProjectMembersList: false) + end + it 'show members from project and group' do project.add_developer(user2) diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index d69c3f2652c..e7970b28e37 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date let(:new_member) { create(:user) } before do + stub_feature_flags(vue_project_members_list: false) + travel_to Time.now.utc.beginning_of_day project.add_maintainer(maintainer) @@ -18,6 +20,8 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date end it 'expiration date is displayed in the members list' do + stub_feature_flags(invite_members_group_modal: false) + visit project_project_members_path(project) page.within '.invite-users-form' do diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index be27cbc0d66..9b9f1f26d66 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Sorting' do let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) } before do + stub_feature_flags(vue_project_members_list: false) + create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) sign_in(maintainer) diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb index bdcf02c82a4..7c72ba31029 100644 --- a/spec/features/projects/members/tabs_spec.rb +++ b/spec/features/projects/members/tabs_spec.rb @@ -20,6 +20,7 @@ RSpec.describe 'Projects > Members > Tabs' do end before do + stub_feature_flags(vue_project_members_list: false) allow(Kaminari.config).to receive(:default_per_page).and_return(1) sign_in(user) diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 25791b393bc..4ff3827b240 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -67,23 +67,4 @@ RSpec.describe 'Project navbar' do it_behaves_like 'verified navigation bar' end - - context 'when invite team members is not available' do - it 'does not display the js-invite-members-trigger' do - visit project_path(project) - - expect(page).not_to have_selector('.js-invite-members-trigger') - end - end - - context 'when invite team members is available' do - it 'includes the div for js-invite-members-trigger' do - stub_feature_flags(invite_members_group_modal: true) - allow_any_instance_of(InviteMembersHelper).to receive(:invite_members_allowed?).and_return(true) - - visit project_path(project) - - expect(page).to have_selector('.js-invite-members-trigger') - end - end end diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb new file mode 100644 index 00000000000..24c9edb79e5 --- /dev/null +++ b/spec/features/projects/pages/user_adds_domain_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'User adds pages domain', :js do + include LetsEncryptHelpers + + let_it_be(:project) { create(:project, pages_https_only: false) } + let(:user) { create(:user) } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + + project.add_maintainer(user) + + sign_in(user) + end + + context 'when pages are exposed on external HTTP address', :http_pages_enabled do + let(:project) { create(:project, pages_https_only: false) } + + shared_examples 'adds new domain' do + it 'adds new domain' do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('my.test.domain.com') + end + end + + it 'allows to add new domain' do + visit project_pages_path(project) + + expect(page).to have_content('New Domain') + end + + it_behaves_like 'adds new domain' + + context 'when project in group namespace' do + it_behaves_like 'adds new domain' do + let(:group) { create :group } + let(:project) { create(:project, namespace: group, pages_https_only: false) } + end + end + + context 'when pages domain is added' do + before do + create(:pages_domain, project: project, domain: 'my.test.domain.com') + + visit new_project_pages_domain_path(project) + end + + it 'renders certificates is disabled' do + expect(page).to have_content('Support for custom certificates is disabled') + end + + it 'does not adds new domain and renders error message' do + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('Domain has already been taken') + end + end + end + + context 'when pages are exposed on external HTTPS address', :https_pages_enabled, :js do + let(:certificate_pem) do + attributes_for(:pages_domain)[:certificate] + end + + let(:certificate_key) do + attributes_for(:pages_domain)[:key] + end + + it 'adds new domain with certificate' do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + + fill_in 'Certificate (PEM)', with: certificate_pem + fill_in 'Key (PEM)', with: certificate_key + click_button 'Create New Domain' + + expect(page).to have_content('my.test.domain.com') + end + + it "adds new domain with certificate if Let's Encrypt is enabled" do + stub_lets_encrypt_settings + + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + + find('.js-auto-ssl-toggle-container .project-feature-toggle').click + + fill_in 'Certificate (PEM)', with: certificate_pem + fill_in 'Key (PEM)', with: certificate_key + click_button 'Create New Domain' + + expect(page).to have_content('my.test.domain.com') + end + + it 'shows validation error if domain is duplicated' do + project.pages_domains.create!(domain: 'my.test.domain.com') + + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('Domain has already been taken') + end + + describe 'with dns verification enabled' do + before do + stub_application_setting(pages_domain_verification_enabled: true) + end + + it 'shows the DNS verification record' do + domain = create(:pages_domain, project: project) + + visit project_pages_path(project) + + within('#content-body') { click_link 'Edit' } + expect(page).to have_field :domain_verification, with: "#{domain.verification_domain} TXT #{domain.keyed_verification_code}" + end + end + + describe 'updating the certificate for an existing domain' do + let!(:domain) do + create(:pages_domain, project: project, auto_ssl_enabled: false) + end + + it 'allows the certificate to be updated' do + visit project_pages_path(project) + + within('#content-body') { click_link 'Edit' } + click_button 'Save Changes' + + expect(page).to have_content('Domain was updated') + end + + context 'when the certificate is invalid' do + let!(:domain) do + create(:pages_domain, :without_certificate, :without_key, project: project) + end + + it 'tells the user what the problem is' do + visit project_pages_path(project) + + within('#content-body') { click_link 'Edit' } + + fill_in 'Certificate (PEM)', with: 'invalid data' + click_button 'Save Changes' + + expect(page).to have_content('Certificate must be a valid PEM certificate') + expect(page).to have_content('Certificate misses intermediates') + expect(page).to have_content("Key doesn't match the certificate") + end + end + + it 'allows the certificate to be removed', :js do + visit project_pages_path(project) + + within('#content-body') { click_link 'Edit' } + + accept_confirm { click_link 'Remove' } + + expect(page).to have_field('Certificate (PEM)', with: '') + expect(page).to have_field('Key (PEM)', with: '') + domain.reload + expect(domain.certificate).to be_nil + expect(domain.key).to be_nil + end + + it 'shows the DNS CNAME record' do + visit project_pages_path(project) + + within('#content-body') { click_link 'Edit' } + expect(page).to have_field :domain_dns, with: "#{domain.domain} CNAME #{domain.project.pages_subdomain}.#{Settings.pages.host}." + end + end + end +end diff --git a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb new file mode 100644 index 00000000000..cf8438d5e6f --- /dev/null +++ b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do + include LetsEncryptHelpers + + let(:project) { create(:project, pages_https_only: false) } + let(:user) { create(:user) } + let(:role) { :maintainer } + let(:certificate_pem) { attributes_for(:pages_domain)[:certificate] } + + let(:certificate_key) { attributes_for(:pages_domain)[:key] } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + stub_lets_encrypt_settings + + project.add_role(user, role) + sign_in(user) + project.namespace.update!(owner: user) + allow_next_instance_of(Project) do |instance| + allow(instance).to receive(:pages_deployed?) { true } + end + end + + it "creates new domain with Let's Encrypt enabled by default" do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + + expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true' + click_button 'Create New Domain' + + expect(page).to have_content('my.test.domain.com') + expect(PagesDomain.find_by_domain('my.test.domain.com').auto_ssl_enabled).to eq(true) + end + + context 'when the auto SSL management is initially disabled' do + let(:domain) do + create(:pages_domain, auto_ssl_enabled: false, project: project) + end + + it 'enables auto SSL and dynamically updates the form accordingly', :js do + visit project_pages_domain_path(project, domain) + + expect(domain.auto_ssl_enabled).to eq false + + expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false' + expect(page).to have_selector '.card-header', text: 'Certificate' + expect(page).to have_text domain.subject + + find('.js-auto-ssl-toggle-container .project-feature-toggle').click + + expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true' + expect(page).not_to have_selector '.card-header', text: 'Certificate' + expect(page).not_to have_text domain.subject + + click_on 'Save Changes' + + expect(domain.reload.auto_ssl_enabled).to eq true + end + end + + context 'when the auto SSL management is initially enabled' do + let(:domain) do + create(:pages_domain, :letsencrypt, auto_ssl_enabled: true, project: project) + end + + it 'disables auto SSL and dynamically updates the form accordingly', :js do + visit project_pages_domain_path(project, domain) + + expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true' + expect(page).not_to have_field 'Certificate (PEM)', type: 'textarea' + expect(page).not_to have_field 'Key (PEM)', type: 'textarea' + + find('.js-auto-ssl-toggle-container .project-feature-toggle').click + + expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false' + expect(page).to have_field 'Certificate (PEM)', type: 'textarea' + expect(page).to have_field 'Key (PEM)', type: 'textarea' + + click_on 'Save Changes' + + expect(domain.reload.auto_ssl_enabled).to eq false + end + end + + context "when we failed to obtain Let's Encrypt certificate", :js do + let(:domain) do + create(:pages_domain, auto_ssl_enabled: true, auto_ssl_failed: true, project: project) + end + + it 'user can retry obtaining certificate' do + visit project_pages_domain_path(project, domain) + + expect(page).to have_text("Something went wrong while obtaining the Let's Encrypt certificate.") + + click_on('Retry') + + expect(page).to have_text("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.") + end + end + + shared_examples 'user sees private keys only for user provided certificate' do + shared_examples 'user do not see private key' do + it 'user do not see private key' do + visit project_pages_domain_path(project, domain) + + expect(page).not_to have_selector '.card-header', text: 'Certificate' + expect(page).not_to have_text domain.subject + end + end + + context 'when auto_ssl is enabled for domain' do + let(:domain) { create(:pages_domain, :letsencrypt, project: project, auto_ssl_enabled: true) } + + include_examples 'user do not see private key' + end + + context 'when auto_ssl is disabled for domain' do + let(:domain) { create(:pages_domain, :letsencrypt, project: project) } + + include_examples 'user do not see private key' + end + + context 'when certificate is provided by user' do + let(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: false) } + + it 'user sees certificate subject' do + visit project_pages_domain_path(project, domain) + + expect(page).to have_selector '.card-header', text: 'Certificate' + expect(page).to have_text domain.subject + end + + it 'user can delete the certificate', :js do + visit project_pages_domain_path(project, domain) + + expect(page).to have_selector '.card-header', text: 'Certificate' + expect(page).to have_text domain.subject + within('.card') { accept_confirm { click_on 'Remove' } } + expect(page).to have_field 'Certificate (PEM)', with: '' + expect(page).to have_field 'Key (PEM)', with: '' + end + end + end + + include_examples 'user sees private keys only for user provided certificate' + + context 'when letsencrypt is disabled' do + let(:domain) do + create(:pages_domain, auto_ssl_enabled: false, project: project) + end + + before do + stub_application_setting(lets_encrypt_terms_of_service_accepted: false) + + visit project_pages_domain_path(project, domain) + end + + it "does not render the Let's Encrypt field", :js do + expect(page).not_to have_selector '.js-auto-ssl-toggle-container' + end + + include_examples 'user sees private keys only for user provided certificate' + end +end diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb new file mode 100644 index 00000000000..3649fae17ce --- /dev/null +++ b/spec/features/projects/pages/user_edits_settings_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Pages edits pages settings', :js do + let(:project) { create(:project, pages_https_only: false) } + let(:user) { create(:user) } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + + project.add_maintainer(user) + + sign_in(user) + end + + context 'when user is the owner' do + before do + project.namespace.update!(owner: user) + end + + context 'when pages deployed' do + before do + project.mark_pages_as_deployed + end + + it 'renders Access pages' do + visit project_pages_path(project) + + expect(page).to have_content('Access pages') + end + + context 'when pages are disabled in the project settings' do + it 'renders disabled warning' do + project.project_feature.update!(pages_access_level: ProjectFeature::DISABLED) + + visit project_pages_path(project) + + expect(page).to have_content('GitLab Pages are disabled for this project') + end + end + + it 'renders first deployment warning' do + visit project_pages_path(project) + + expect(page).to have_content('It may take up to 30 minutes before the site is available after the first deployment.') + end + + shared_examples 'does not render access control warning' do + it 'does not render access control warning' do + visit project_pages_path(project) + + expect(page).not_to have_content('Access Control is enabled for this Pages website') + end + end + + include_examples 'does not render access control warning' + + context 'when access control is enabled in gitlab settings' do + before do + stub_pages_setting(access_control: true) + end + + it 'renders access control warning' do + visit project_pages_path(project) + + expect(page).to have_content('Access Control is enabled for this Pages website') + end + + context 'when pages are public' do + before do + project.project_feature.update!(pages_access_level: ProjectFeature::PUBLIC) + end + + include_examples 'does not render access control warning' + end + end + + context 'when support for external domains is disabled' do + it 'renders message that support is disabled' do + visit project_pages_path(project) + + expect(page).to have_content('Support for domains and certificates is disabled') + end + end + end + + it 'does not see anything to destroy' do + visit project_pages_path(project) + + expect(page).to have_content('Configure pages') + expect(page).not_to have_link('Remove pages') + end + + describe 'project settings page' do + it 'renders "Pages" tab' do + visit edit_project_path(project) + + page.within '.nav-sidebar' do + expect(page).to have_link('Pages') + end + end + + context 'when pages are disabled' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + end + + it 'does not render "Pages" tab' do + visit edit_project_path(project) + + page.within '.nav-sidebar' do + expect(page).not_to have_link('Pages') + end + end + end + end + end + + describe 'HTTPS settings', :https_pages_enabled do + before do + project.namespace.update!(owner: user) + + project.mark_pages_as_deployed + end + + it 'tries to change the setting' do + visit project_pages_path(project) + expect(page).to have_content("Force HTTPS (requires valid certificates)") + + uncheck :project_pages_https_only + + click_button 'Save' + + expect(page).to have_text('Your changes have been saved') + expect(page).not_to have_checked_field('project_pages_https_only') + end + + context 'setting could not be updated' do + let(:service) { instance_double('Projects::UpdateService') } + + before do + allow(Projects::UpdateService).to receive(:new).and_return(service) + allow(service).to receive(:execute).and_return(status: :error, message: 'Some error has occured') + end + + it 'tries to change the setting' do + visit project_pages_path(project) + + uncheck :project_pages_https_only + + click_button 'Save' + + expect(page).to have_text('Some error has occured') + end + end + + context 'non-HTTPS domain exists' do + let(:project) { create(:project, pages_https_only: false) } + + before do + create(:pages_domain, :without_key, :without_certificate, project: project) + end + + it 'the setting is disabled' do + visit project_pages_path(project) + + expect(page).to have_field(:project_pages_https_only, disabled: true) + expect(page).to have_button('Save') + end + end + + context 'HTTPS pages are disabled', :https_pages_disabled do + it 'the setting is unavailable' do + visit project_pages_path(project) + + expect(page).not_to have_field(:project_pages_https_only) + expect(page).not_to have_content('Force HTTPS (requires valid certificates)') + expect(page).to have_button('Save') + end + end + end + + describe 'Remove page' do + context 'when pages are deployed' do + before do + project.mark_pages_as_deployed + end + + it 'removes the pages', :sidekiq_inline do + visit project_pages_path(project) + + expect(page).to have_link('Remove pages') + + accept_confirm { click_link 'Remove pages' } + + expect(page).to have_content('Pages were scheduled for removal') + expect(project.reload.pages_deployed?).to be_falsey + end + end + end +end diff --git a/spec/features/projects/pages_lets_encrypt_spec.rb b/spec/features/projects/pages_lets_encrypt_spec.rb deleted file mode 100644 index 302e9f5e533..00000000000 --- a/spec/features/projects/pages_lets_encrypt_spec.rb +++ /dev/null @@ -1,167 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do - include LetsEncryptHelpers - - let(:project) { create(:project, pages_https_only: false) } - let(:user) { create(:user) } - let(:role) { :maintainer } - let(:certificate_pem) { attributes_for(:pages_domain)[:certificate] } - - let(:certificate_key) { attributes_for(:pages_domain)[:key] } - - before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) - stub_lets_encrypt_settings - - project.add_role(user, role) - sign_in(user) - project.namespace.update(owner: user) - allow_next_instance_of(Project) do |instance| - allow(instance).to receive(:pages_deployed?) { true } - end - end - - it "creates new domain with Let's Encrypt enabled by default" do - visit new_project_pages_domain_path(project) - - fill_in 'Domain', with: 'my.test.domain.com' - - expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true' - click_button 'Create New Domain' - - expect(page).to have_content('my.test.domain.com') - expect(PagesDomain.find_by_domain('my.test.domain.com').auto_ssl_enabled).to eq(true) - end - - context 'when the auto SSL management is initially disabled' do - let(:domain) do - create(:pages_domain, auto_ssl_enabled: false, project: project) - end - - it 'enables auto SSL and dynamically updates the form accordingly', :js do - visit project_pages_domain_path(project, domain) - - expect(domain.auto_ssl_enabled).to eq false - - expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false' - expect(page).to have_selector '.card-header', text: 'Certificate' - expect(page).to have_text domain.subject - - find('.js-auto-ssl-toggle-container .project-feature-toggle').click - - expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true' - expect(page).not_to have_selector '.card-header', text: 'Certificate' - expect(page).not_to have_text domain.subject - - click_on 'Save Changes' - - expect(domain.reload.auto_ssl_enabled).to eq true - end - end - - context 'when the auto SSL management is initially enabled' do - let(:domain) do - create(:pages_domain, :letsencrypt, auto_ssl_enabled: true, project: project) - end - - it 'disables auto SSL and dynamically updates the form accordingly', :js do - visit project_pages_domain_path(project, domain) - - expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true' - expect(page).not_to have_field 'Certificate (PEM)', type: 'textarea' - expect(page).not_to have_field 'Key (PEM)', type: 'textarea' - - find('.js-auto-ssl-toggle-container .project-feature-toggle').click - - expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false' - expect(page).to have_field 'Certificate (PEM)', type: 'textarea' - expect(page).to have_field 'Key (PEM)', type: 'textarea' - - click_on 'Save Changes' - - expect(domain.reload.auto_ssl_enabled).to eq false - end - end - - context "when we failed to obtain Let's Encrypt certificate", :js do - let(:domain) do - create(:pages_domain, auto_ssl_enabled: true, auto_ssl_failed: true, project: project) - end - - it 'user can retry obtaining certificate' do - visit project_pages_domain_path(project, domain) - - expect(page).to have_text("Something went wrong while obtaining the Let's Encrypt certificate.") - - click_on('Retry') - - expect(page).to have_text("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.") - end - end - - shared_examples 'user sees private keys only for user provided certificate' do - shared_examples 'user do not see private key' do - it 'user do not see private key' do - visit project_pages_domain_path(project, domain) - - expect(page).not_to have_selector '.card-header', text: 'Certificate' - expect(page).not_to have_text domain.subject - end - end - - context 'when auto_ssl is enabled for domain' do - let(:domain) { create(:pages_domain, :letsencrypt, project: project, auto_ssl_enabled: true) } - - include_examples 'user do not see private key' - end - - context 'when auto_ssl is disabled for domain' do - let(:domain) { create(:pages_domain, :letsencrypt, project: project) } - - include_examples 'user do not see private key' - end - - context 'when certificate is provided by user' do - let(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: false) } - - it 'user sees certificate subject' do - visit project_pages_domain_path(project, domain) - - expect(page).to have_selector '.card-header', text: 'Certificate' - expect(page).to have_text domain.subject - end - - it 'user can delete the certificate', :js do - visit project_pages_domain_path(project, domain) - - expect(page).to have_selector '.card-header', text: 'Certificate' - expect(page).to have_text domain.subject - within('.card') { accept_confirm { click_on 'Remove' } } - expect(page).to have_field 'Certificate (PEM)', with: '' - expect(page).to have_field 'Key (PEM)', with: '' - end - end - end - - include_examples 'user sees private keys only for user provided certificate' - - context 'when letsencrypt is disabled' do - let(:domain) do - create(:pages_domain, auto_ssl_enabled: false, project: project) - end - - before do - stub_application_setting(lets_encrypt_terms_of_service_accepted: false) - - visit project_pages_domain_path(project, domain) - end - - it "does not render the Let's Encrypt field", :js do - expect(page).not_to have_selector '.js-auto-ssl-toggle-container' - end - - include_examples 'user sees private keys only for user provided certificate' - end -end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb deleted file mode 100644 index 11f712fde81..00000000000 --- a/spec/features/projects/pages_spec.rb +++ /dev/null @@ -1,411 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.shared_examples 'pages settings editing' do - let_it_be(:project) { create(:project, pages_https_only: false) } - let(:user) { create(:user) } - let(:role) { :maintainer } - - before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) - - project.add_role(user, role) - - sign_in(user) - end - - context 'when user is the owner' do - before do - project.namespace.update(owner: user) - end - - context 'when pages deployed' do - before do - allow_any_instance_of(Project).to receive(:pages_deployed?) { true } - end - - it 'renders Access pages' do - visit project_pages_path(project) - - expect(page).to have_content('Access pages') - end - - context 'when pages are disabled in the project settings' do - it 'renders disabled warning' do - project.project_feature.update!(pages_access_level: ProjectFeature::DISABLED) - - visit project_pages_path(project) - - expect(page).to have_content('GitLab Pages are disabled for this project') - end - end - - it 'renders first deployment warning' do - visit project_pages_path(project) - - expect(page).to have_content('It may take up to 30 minutes before the site is available after the first deployment.') - end - - shared_examples 'does not render access control warning' do - it 'does not render access control warning' do - visit project_pages_path(project) - - expect(page).not_to have_content('Access Control is enabled for this Pages website') - end - end - - include_examples 'does not render access control warning' - - context 'when access control is enabled in gitlab settings' do - before do - stub_pages_setting(access_control: true) - end - - it 'renders access control warning' do - visit project_pages_path(project) - - expect(page).to have_content('Access Control is enabled for this Pages website') - end - - context 'when pages are public' do - before do - project.project_feature.update!(pages_access_level: ProjectFeature::PUBLIC) - end - - include_examples 'does not render access control warning' - end - end - - context 'when support for external domains is disabled' do - it 'renders message that support is disabled' do - visit project_pages_path(project) - - expect(page).to have_content('Support for domains and certificates is disabled') - end - end - - context 'when pages are exposed on external HTTP address', :http_pages_enabled do - let(:project) { create(:project, pages_https_only: false) } - - shared_examples 'adds new domain' do - it 'adds new domain' do - visit new_project_pages_domain_path(project) - - fill_in 'Domain', with: 'my.test.domain.com' - click_button 'Create New Domain' - - expect(page).to have_content('my.test.domain.com') - end - end - - it 'allows to add new domain' do - visit project_pages_path(project) - - expect(page).to have_content('New Domain') - end - - it_behaves_like 'adds new domain' - - context 'when project in group namespace' do - it_behaves_like 'adds new domain' do - let(:group) { create :group } - let(:project) { create(:project, namespace: group, pages_https_only: false) } - end - end - - context 'when pages domain is added' do - before do - create(:pages_domain, project: project, domain: 'my.test.domain.com') - - visit new_project_pages_domain_path(project) - end - - it 'renders certificates is disabled' do - expect(page).to have_content('Support for custom certificates is disabled') - end - - it 'does not adds new domain and renders error message' do - fill_in 'Domain', with: 'my.test.domain.com' - click_button 'Create New Domain' - - expect(page).to have_content('Domain has already been taken') - end - end - end - - context 'when pages are exposed on external HTTPS address', :https_pages_enabled, :js do - let(:certificate_pem) do - attributes_for(:pages_domain)[:certificate] - end - - let(:certificate_key) do - attributes_for(:pages_domain)[:key] - end - - it 'adds new domain with certificate' do - visit new_project_pages_domain_path(project) - - fill_in 'Domain', with: 'my.test.domain.com' - - if ::Gitlab::LetsEncrypt.enabled? - find('.js-auto-ssl-toggle-container .project-feature-toggle').click - end - - fill_in 'Certificate (PEM)', with: certificate_pem - fill_in 'Key (PEM)', with: certificate_key - click_button 'Create New Domain' - - expect(page).to have_content('my.test.domain.com') - end - - it 'shows validation error if domain is duplicated' do - project.pages_domains.create!(domain: 'my.test.domain.com') - - visit new_project_pages_domain_path(project) - - fill_in 'Domain', with: 'my.test.domain.com' - click_button 'Create New Domain' - - expect(page).to have_content('Domain has already been taken') - end - - describe 'with dns verification enabled' do - before do - stub_application_setting(pages_domain_verification_enabled: true) - end - - it 'shows the DNS verification record' do - domain = create(:pages_domain, project: project) - - visit project_pages_path(project) - - within('#content-body') { click_link 'Edit' } - expect(page).to have_field :domain_verification, with: "#{domain.verification_domain} TXT #{domain.keyed_verification_code}" - end - end - - describe 'updating the certificate for an existing domain' do - let!(:domain) do - create(:pages_domain, project: project, auto_ssl_enabled: false) - end - - it 'allows the certificate to be updated' do - visit project_pages_path(project) - - within('#content-body') { click_link 'Edit' } - click_button 'Save Changes' - - expect(page).to have_content('Domain was updated') - end - - context 'when the certificate is invalid' do - let!(:domain) do - create(:pages_domain, :without_certificate, :without_key, project: project) - end - - it 'tells the user what the problem is' do - visit project_pages_path(project) - - within('#content-body') { click_link 'Edit' } - - if ::Gitlab::LetsEncrypt.enabled? - find('.js-auto-ssl-toggle-container .project-feature-toggle').click - end - - fill_in 'Certificate (PEM)', with: 'invalid data' - click_button 'Save Changes' - - expect(page).to have_content('Certificate must be a valid PEM certificate') - expect(page).to have_content('Certificate misses intermediates') - expect(page).to have_content("Key doesn't match the certificate") - end - end - - it 'allows the certificate to be removed', :js do - visit project_pages_path(project) - - within('#content-body') { click_link 'Edit' } - - accept_confirm { click_link 'Remove' } - - expect(page).to have_field('Certificate (PEM)', with: '') - expect(page).to have_field('Key (PEM)', with: '') - domain.reload - expect(domain.certificate).to be_nil - expect(domain.key).to be_nil - end - - it 'shows the DNS CNAME record' do - visit project_pages_path(project) - - within('#content-body') { click_link 'Edit' } - expect(page).to have_field :domain_dns, with: "#{domain.domain} CNAME #{domain.project.pages_subdomain}.#{Settings.pages.host}." - end - end - end - end - - it 'does not see anything to destroy' do - visit project_pages_path(project) - - expect(page).to have_content('Configure pages') - expect(page).not_to have_link('Remove pages') - end - - describe 'project settings page' do - it 'renders "Pages" tab' do - visit edit_project_path(project) - - page.within '.nav-sidebar' do - expect(page).to have_link('Pages') - end - end - - context 'when pages are disabled' do - before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(false) - end - - it 'does not render "Pages" tab' do - visit edit_project_path(project) - - page.within '.nav-sidebar' do - expect(page).not_to have_link('Pages') - end - end - end - end - end - - describe 'HTTPS settings', :https_pages_enabled do - before do - project.namespace.update(owner: user) - - allow_any_instance_of(Project).to receive(:pages_deployed?) { true } - end - - it 'tries to change the setting' do - visit project_pages_path(project) - expect(page).to have_content("Force HTTPS (requires valid certificates)") - - uncheck :project_pages_https_only - - click_button 'Save' - - expect(page).to have_text('Your changes have been saved') - expect(page).not_to have_checked_field('project_pages_https_only') - end - - context 'setting could not be updated' do - let(:service) { instance_double('Projects::UpdateService') } - - before do - allow(Projects::UpdateService).to receive(:new).and_return(service) - allow(service).to receive(:execute).and_return(status: :error, message: 'Some error has occured') - end - - it 'tries to change the setting' do - visit project_pages_path(project) - - uncheck :project_pages_https_only - - click_button 'Save' - - expect(page).to have_text('Some error has occured') - end - end - - context 'non-HTTPS domain exists' do - let(:project) { create(:project, pages_https_only: false) } - - before do - create(:pages_domain, :without_key, :without_certificate, project: project) - end - - it 'the setting is disabled' do - visit project_pages_path(project) - - expect(page).to have_field(:project_pages_https_only, disabled: true) - expect(page).to have_button('Save') - end - end - - context 'HTTPS pages are disabled', :https_pages_disabled do - it 'the setting is unavailable' do - visit project_pages_path(project) - - expect(page).not_to have_field(:project_pages_https_only) - expect(page).not_to have_content('Force HTTPS (requires valid certificates)') - expect(page).to have_button('Save') - end - end - end - - describe 'Remove page' do - let(:project) { create :project, :repository } - - context 'when pages are deployed' do - let(:pipeline) do - commit_sha = project.commit('HEAD').sha - - project.ci_pipelines.create( - ref: 'HEAD', - sha: commit_sha, - source: :push, - protected: false - ) - end - - let(:ci_build) do - create( - :ci_build, - project: project, - pipeline: pipeline, - ref: 'HEAD') - end - - let!(:artifact) do - create(:ci_job_artifact, :archive, :correct_checksum, - file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), job: ci_build) - end - - let!(:metadata) do - create(:ci_job_artifact, :metadata, - file: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')), job: ci_build) - end - - before do - result = Projects::UpdatePagesService.new(project, ci_build).execute - expect(result[:status]).to eq(:success) - expect(project).to be_pages_deployed - end - - it 'removes the pages', :sidekiq_inline do - visit project_pages_path(project) - - expect(page).to have_link('Remove pages') - - accept_confirm { click_link 'Remove pages' } - - expect(page).to have_content('Pages were scheduled for removal') - expect(project.reload.pages_deployed?).to be_falsey - end - end - end -end - -RSpec.describe 'Pages', :js do - include LetsEncryptHelpers - - context 'when editing normally' do - include_examples 'pages settings editing' - end - - context 'when letsencrypt support is enabled' do - before do - stub_lets_encrypt_settings - end - - include_examples 'pages settings editing' - end -end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index ac3566fbbdd..e1d68a3f12e 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -15,6 +15,7 @@ RSpec.describe 'Pipeline', :js do sign_in(user) project.add_role(user, role) stub_feature_flags(graphql_pipeline_details: false) + stub_feature_flags(graphql_pipeline_details_users: false) end shared_context 'pipeline builds' do @@ -625,20 +626,6 @@ RSpec.describe 'Pipeline', :js do end end end - - context 'when FF dag_pipeline_tab is disabled' do - before do - stub_feature_flags(dag_pipeline_tab: false) - visit_pipeline - end - - it 'does not show DAG link' do - expect(page).to have_link('Pipeline') - expect(page).to have_link('Jobs') - expect(page).not_to have_link('DAG') - expect(page).to have_link('Failed Jobs') - end - end end context 'when user does not have access to read jobs' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 450524b8d70..e0a0591fe6b 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'Pipelines', :js do before do sign_in(user) stub_feature_flags(graphql_pipeline_details: false) + stub_feature_flags(graphql_pipeline_details_users: false) project.add_developer(user) project.update!(auto_devops_attributes: { enabled: false }) end @@ -287,23 +288,23 @@ RSpec.describe 'Pipelines', :js do end it 'has a dropdown with play button' do - expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play') + expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]') end it 'has link to the manual action' do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click expect(page).to have_button('manual build') end context 'when manual action was played' do before do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click click_button('manual build') end it 'enqueues manual action job' do - expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled') + expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] .gl-dropdown-toggle:disabled') end end end @@ -321,11 +322,11 @@ RSpec.describe 'Pipelines', :js do end it 'has a dropdown for actionable jobs' do - expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play') + expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]') end it "has link to the delayed job's action" do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click time_diff = [0, delayed_job.scheduled_at - Time.now].max expect(page).to have_button('delayed job 1') @@ -341,7 +342,7 @@ RSpec.describe 'Pipelines', :js do end it "shows 00:00:00 as the remaining time" do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click expect(page).to have_content("00:00:00") end @@ -349,7 +350,7 @@ RSpec.describe 'Pipelines', :js do context 'when user played a delayed job immediately' do before do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click page.accept_confirm { click_button('delayed job 1') } wait_for_requests end @@ -517,56 +518,75 @@ RSpec.describe 'Pipelines', :js do end end - context 'mini pipeline graph' do - let!(:build) do - create(:ci_build, :pending, pipeline: pipeline, - stage: 'build', - name: 'build') - end - - before do - visit_project_pipelines - end + shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled| + context 'mini pipeline graph' do + let!(:build) do + create(:ci_build, :pending, pipeline: pipeline, + stage: 'build', + name: 'build') + end - it 'renders a mini pipeline graph' do - expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]') - expect(page).to have_selector('.js-builds-dropdown-button') - end + before do + stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled) + visit_project_pipelines + end - context 'when clicking a stage badge' do - it 'opens a dropdown' do - find('.js-builds-dropdown-button').click + let_it_be(:dropdown_toggle_selector) do + if ci_mini_pipeline_gl_dropdown_enabled + '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle' + else + '[data-testid="mini-pipeline-graph-dropdown-toggle"]' + end + end - expect(page).to have_link build.name + it 'renders a mini pipeline graph' do + expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]') + expect(page).to have_selector(dropdown_toggle_selector) end - it 'is possible to cancel pending build' do - find('.js-builds-dropdown-button').click - find('.js-ci-action').click - wait_for_requests + context 'when clicking a stage badge' do + it 'opens a dropdown' do + find(dropdown_toggle_selector).click - expect(build.reload).to be_canceled - end - end + expect(page).to have_link build.name + end - context 'for a failed pipeline' do - let!(:build) do - create(:ci_build, :failed, pipeline: pipeline, - stage: 'build', - name: 'build') + it 'is possible to cancel pending build' do + find(dropdown_toggle_selector).click + find('.js-ci-action').click + wait_for_requests + + expect(build.reload).to be_canceled + end end - it 'displays the failure reason' do - find('.js-builds-dropdown-button').click + context 'for a failed pipeline' do + let!(:build) do + create(:ci_build, :failed, pipeline: pipeline, + stage: 'build', + name: 'build') + end + + it 'displays the failure reason' do + find(dropdown_toggle_selector).click - within('.js-builds-dropdown-list') do - build_element = page.find('.mini-pipeline-graph-dropdown-item') - expect(build_element['title']).to eq('build - failed - (unknown failure)') + within('.js-builds-dropdown-list') do + build_element = page.find('.mini-pipeline-graph-dropdown-item') + expect(build_element['title']).to eq('build - failed - (unknown failure)') + end end end end end + context 'with ci_mini_pipeline_gl_dropdown disabled' do + it_behaves_like "mini pipeline renders", false + end + + context 'with ci_mini_pipeline_gl_dropdown enabled' do + it_behaves_like "mini pipeline renders", true + end + context 'with pagination' do before do allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb index 3994f55caee..4dfd4416eeb 100644 --- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb @@ -40,4 +40,8 @@ RSpec.describe 'Slack slash commands', :js do value = find_field('url').value expect(value).to match("api/v4/projects/#{project.id}/services/slack_slash_commands/trigger") end + + it 'shows help content' do + expect(page).to have_content('This service allows users to perform common operations on this project by entering slash commands in Slack.') + end end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index c087237fd7c..39c4315bf0f 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -46,7 +46,7 @@ RSpec.describe "Projects > Settings > Pipelines settings" do it 'updates auto_cancel_pending_pipelines' do visit project_settings_ci_cd_path(project) - page.check('Auto-cancel redundant, pending pipelines') + page.check('Auto-cancel redundant pipelines') page.within '#js-general-pipeline-settings' do click_on 'Save changes' end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 3e520142117..2f257d299d8 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do click_button 'Add key' expect(page).to have_content('new_deploy_key') - expect(page).to have_content('Write access allowed') + expect(page).to have_content('Grant write permissions to this key') end it 'edit an existing deploy key' do @@ -77,7 +77,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do click_button 'Save changes' expect(page).to have_content('updated_deploy_key') - expect(page).to have_content('Write access allowed') + expect(page).to have_content('Grant write permissions to this key') end it 'edit an existing public deploy key to be writable' do @@ -90,7 +90,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do click_button 'Save changes' expect(page).to have_content('public_deploy_key') - expect(page).to have_content('Write access allowed') + expect(page).to have_content('Grant write permissions to this key') end it 'edit a deploy key from projects user has access to' do diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index 726b8fb6840..a4abdf9f571 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -11,6 +11,8 @@ RSpec.describe 'Projects > Settings > User manages project members' do let(:user_mike) { create(:user, name: 'Mike') } before do + stub_feature_flags(vue_project_members_list: false) + project.add_maintainer(user) project.add_developer(user_dmitriy) sign_in(user) @@ -37,6 +39,8 @@ RSpec.describe 'Projects > Settings > User manages project members' do end it 'imports a team from another project' do + stub_feature_flags(invite_members_group_modal: false) + project2.add_maintainer(user) project2.add_reporter(user_mike) diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb index d444ea27d35..5f7d9b0963b 100644 --- a/spec/features/projects/show/user_manages_notifications_spec.rb +++ b/spec/features/projects/show/user_manages_notifications_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do let(:project) { create(:project, :public, :repository) } before do + stub_feature_flags(vue_notification_dropdown: false) sign_in(project.owner) end diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb index febdb70de86..e6157887c12 100644 --- a/spec/features/projects/show/user_sees_git_instructions_spec.rb +++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb @@ -5,6 +5,13 @@ require 'spec_helper' RSpec.describe 'Projects > Show > User sees Git instructions' do let_it_be(:user) { create(:user) } + before do + # Reset user notification settings between examples to prevent + # validation failure on NotificationSetting. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/299822#note_492817174 + user.notification_settings.reset + end + shared_examples_for 'redirects to the sign in page' do it 'redirects to the sign in page' do expect(current_path).to eq(new_user_session_path) diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb index b860cd08e64..1a882050126 100644 --- a/spec/features/search/user_searches_for_commits_spec.rb +++ b/spec/features/search/user_searches_for_commits_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'User searches for commits' do +RSpec.describe 'User searches for commits', :js do let(:project) { create(:project, :repository) } let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:user) { create(:user) } @@ -41,7 +41,7 @@ RSpec.describe 'User searches for commits' do submit_search('See merge request') select_search_scope('Commits') - expect(page).to have_selector('.commit-row-description', count: 9) + expect(page).to have_selector('.commit-row-description', visible: false, count: 9) end end end diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index e253b9f2f7a..828e478d701 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -75,7 +75,7 @@ RSpec.describe 'User searches for issues', :js do expect(page.all('.search-result-row').last).to have_link(issue1.title) end - find('.reverse-sort-btn').click + find('[data-testid="sort-highest-icon"]').click page.within('.results') do expect(page.all('.search-result-row').first).to have_link(issue1.title) diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index 21e8075739f..7271716644b 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -5,8 +5,14 @@ require 'spec_helper' RSpec.describe 'User searches for merge requests', :js do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } - let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) } - let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) } + let!(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) } + let!(:merge_request2) { create(:merge_request, :simple, title: 'Merge Request Bar', source_project: project, target_project: project) } + + def search_for_mr(search) + fill_in('dashboard_search', with: search) + find('.btn-search').click + select_search_scope('Merge requests') + end before do project.add_maintainer(user) @@ -18,9 +24,7 @@ RSpec.describe 'User searches for merge requests', :js do include_examples 'top right search form' it 'finds a merge request' do - fill_in('dashboard_search', with: merge_request1.title) - find('.btn-search').click - select_search_scope('Merge requests') + search_for_mr(merge_request1.title) page.within('.results') do expect(page).to have_link(merge_request1.title) @@ -28,6 +32,22 @@ RSpec.describe 'User searches for merge requests', :js do end end + it 'sorts by created date' do + search_for_mr('Merge Request') + + page.within('.results') do + expect(page.all('.search-result-row').first).to have_link(merge_request2.title) + expect(page.all('.search-result-row').last).to have_link(merge_request1.title) + end + + find('[data-testid="sort-highest-icon"]').click + + page.within('.results') do + expect(page.all('.search-result-row').first).to have_link(merge_request1.title) + expect(page.all('.search-result-row').last).to have_link(merge_request2.title) + end + end + context 'when on a project page' do it 'finds a merge request' do find('[data-testid="project-filter"]').click @@ -38,9 +58,7 @@ RSpec.describe 'User searches for merge requests', :js do click_on(project.full_name) end - fill_in('dashboard_search', with: merge_request1.title) - find('.btn-search').click - select_search_scope('Merge requests') + search_for_mr(merge_request1.title) page.within('.results') do expect(page).to have_link(merge_request1.title) diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb index b64909dd42f..e34ae031679 100644 --- a/spec/features/search/user_searches_for_projects_spec.rb +++ b/spec/features/search/user_searches_for_projects_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'User searches for projects' do +RSpec.describe 'User searches for projects', :js do let!(:project) { create(:project, :public, name: 'Shop') } context 'when signed out' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 0f8daaf8e15..e17521e1d02 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -69,7 +69,13 @@ RSpec.describe 'Task Lists', :js do wait_for_requests expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector('.btn-close') + end + + it_behaves_like 'page with comment and close button', 'Close issue' do + def setup + visit_issue(project, issue) + wait_for_requests + end end it 'is only editable by author' do diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 5762a54a717..eed67e3ac78 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -39,6 +39,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j end it 'allows the same device to be registered for multiple users' do + # U2f specs will be removed after WebAuthn migration completed + pending('FakeU2fDevice has static key handle, '\ + 'leading to duplicate credential_xid for WebAuthn during migration, '\ + 'resulting in unique constraint violation') + # First user visit profile_account_path manage_two_factor_authentication @@ -148,6 +153,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j describe "and also the current user" do it "allows logging in with that particular device" do + # U2f specs will be removed after WebAuthn migration completed + pending('FakeU2fDevice has static key handle, '\ + 'leading to duplicate credential_xid for WebAuthn during migration, '\ + 'resulting in unique constraint violation') + # Register current user with the same U2F device current_user = gitlab_sign_in(:user) current_user.update_attribute(:otp_required_for_login, true) diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb index 331f51dad95..5edf8358244 100644 --- a/spec/features/user_sees_revert_modal_spec.rb +++ b/spec/features/user_sees_revert_modal_spec.rb @@ -7,26 +7,33 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } + shared_examples 'showing the revert modal' do + it 'shows the revert modal' do + click_button('Revert') + + page.within('[data-testid="modal-commit"]') do + expect(page).to have_content 'Revert this merge request' + end + end + end + before do sign_in(user) visit(project_merge_request_path(project, merge_request)) click_button('Merge') wait_for_requests - - visit(merge_request_path(merge_request)) - click_link('Revert') end - it 'shows the revert modal' do - page.within('.modal-header') do - expect(page).to have_content 'Revert this merge request' - end + context 'without page reload after merge validates js correctly loaded' do + it_behaves_like 'showing the revert modal' end - it 'closes the revert modal with escape keypress' do - find('#modal-revert-commit').send_keys(:escape) + context 'with page reload validates js correctly loaded' do + before do + visit(merge_request_path(merge_request)) + end - expect(page).not_to have_selector('#modal-revert-commit', visible: true) + it_behaves_like 'showing the revert modal' end end diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb index 2ffb6bb3477..4eebc9d2c1e 100644 --- a/spec/features/webauthn_spec.rb +++ b/spec/features/webauthn_spec.rb @@ -129,6 +129,10 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js do end it 'falls back to U2F' do + # WebAuthn registration is automatically created with the U2fRegistration because of the after_create callback + # so we need to delete it + WebauthnRegistration.delete_all + gitlab_sign_in(user) u2f_device.respond_to_u2f_authentication diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb new file mode 100644 index 00000000000..7c5625486f5 --- /dev/null +++ b/spec/features/whats_new_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "renders a `whats new` dropdown item", :js do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'shows notification dot and count and removes it once viewed' do + visit root_dashboard_path + + page.within '.header-help' do + expect(page).to have_selector('.notification-dot', visible: true) + + find('.header-help-dropdown-toggle').click + + expect(page).to have_button(text: "What's new") + expect(page).to have_selector('.js-whats-new-notification-count') + + find('button', text: "What's new").click + end + + find('.whats-new-drawer .gl-drawer-close-button').click + find('.header-help-dropdown-toggle').click + + page.within '.header-help' do + expect(page).not_to have_selector('.notification-dot', visible: true) + expect(page).to have_button(text: "What's new") + expect(page).not_to have_selector('.js-whats-new-notification-count') + end + end +end diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb index 357b6dfcea2..28bd7e12916 100644 --- a/spec/finders/autocomplete/users_finder_spec.rb +++ b/spec/finders/autocomplete/users_finder_spec.rb @@ -118,5 +118,10 @@ RSpec.describe Autocomplete::UsersFinder do it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) } end + + it 'preloads the status association' do + associations = subject.map { |user| user.association(:status) } + expect(associations).to all(be_loaded) + end end end diff --git a/spec/finders/merge_request/metrics_finder_spec.rb b/spec/finders/merge_request/metrics_finder_spec.rb new file mode 100644 index 00000000000..ea039462e66 --- /dev/null +++ b/spec/finders/merge_request/metrics_finder_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequest::MetricsFinder do + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:merge_request_not_merged) { create(:merge_request, :unique_branches, source_project: project) } + let_it_be(:merged_at) { Time.new(2020, 5, 1) } + let_it_be(:merge_request_merged) do + create(:merge_request, :unique_branches, :merged, source_project: project).tap do |mr| + mr.metrics.update!(merged_at: merged_at) + end + end + + let(:params) do + { + target_project: project, + merged_after: merged_at - 10.days, + merged_before: merged_at + 10.days + } + end + + subject { described_class.new(current_user, params).execute.to_a } + + context 'when target project is missing' do + before do + params.delete(:target_project) + end + + it { is_expected.to be_empty } + end + + context 'when the user is not part of the project' do + it { is_expected.to be_empty } + end + + context 'when user is part of the project' do + before do + project.add_developer(current_user) + end + + it 'returns merge request records' do + is_expected.to eq([merge_request_merged.metrics]) + end + + it 'excludes not merged records' do + is_expected.not_to eq([merge_request_not_merged.metrics]) + end + + context 'when only merged_before is given' do + before do + params.delete(:merged_after) + end + + it { is_expected.to eq([merge_request_merged.metrics]) } + end + + context 'when only merged_after is given' do + before do + params.delete(:merged_before) + end + + it { is_expected.to eq([merge_request_merged.metrics]) } + end + + context 'when no records matching the date range' do + before do + params[:merged_before] = merged_at - 1.year + params[:merged_after] = merged_at - 2.years + end + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb b/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb new file mode 100644 index 00000000000..4e9d021fa5d --- /dev/null +++ b/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::OldestPerCommitFinder do + describe '#execute' do + it 'returns a Hash mapping commit SHAs to their oldest merge requests' do + project = create(:project) + mr1 = create(:merge_request, :merged, target_project: project) + mr2 = create(:merge_request, :merged, target_project: project) + mr1_diff = create(:merge_request_diff, merge_request: mr1) + mr2_diff = create(:merge_request_diff, merge_request: mr2) + sha1 = Digest::SHA1.hexdigest('foo') + sha2 = Digest::SHA1.hexdigest('bar') + + create(:merge_request_diff_commit, merge_request_diff: mr1_diff, sha: sha1) + create(:merge_request_diff_commit, merge_request_diff: mr2_diff, sha: sha1) + create( + :merge_request_diff_commit, + merge_request_diff: mr2_diff, + sha: sha2, + relative_order: 1 + ) + + commits = [double(:commit, id: sha1), double(:commit, id: sha2)] + + expect(described_class.new(project).execute(commits)).to eq( + sha1 => mr1, + sha2 => mr2 + ) + end + + it 'skips merge requests that are not merged' do + mr = create(:merge_request) + mr_diff = create(:merge_request_diff, merge_request: mr) + sha = Digest::SHA1.hexdigest('foo') + + create(:merge_request_diff_commit, merge_request_diff: mr_diff, sha: sha) + + commits = [double(:commit, id: sha)] + + expect(described_class.new(mr.target_project).execute(commits)) + .to be_empty + end + end +end diff --git a/spec/finders/repositories/commits_with_trailer_finder_spec.rb b/spec/finders/repositories/commits_with_trailer_finder_spec.rb new file mode 100644 index 00000000000..0c457aae340 --- /dev/null +++ b/spec/finders/repositories/commits_with_trailer_finder_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Repositories::CommitsWithTrailerFinder do + let(:project) { create(:project, :repository) } + + describe '#each_page' do + it 'only yields commits with the given trailer' do + finder = described_class.new( + project: project, + from: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + to: 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd' + ) + + commits = finder.each_page('Signed-off-by').to_a.flatten + + expect(commits.length).to eq(1) + expect(commits.first.id).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + expect(commits.first.trailers).to eq( + 'Signed-off-by' => 'Dmitriy Zaporozhets ' + ) + end + + it 'supports paginating of commits' do + finder = described_class.new( + project: project, + from: 'c1acaa58bbcbc3eafe538cb8274ba387047b69f8', + to: '5937ac0a7beb003549fc5fd26fc247adbce4a52e', + per_page: 1 + ) + + commits = finder.each_page('Signed-off-by') + + expect(commits.count).to eq(4) + end + end +end diff --git a/spec/finders/terraform/states_finder_spec.rb b/spec/finders/terraform/states_finder_spec.rb new file mode 100644 index 00000000000..260e5f4818f --- /dev/null +++ b/spec/finders/terraform/states_finder_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Terraform::StatesFinder do + describe '#execute' do + let_it_be(:project) { create(:project) } + let_it_be(:state_1) { create(:terraform_state, project: project) } + let_it_be(:state_2) { create(:terraform_state, project: project) } + + let(:user) { project.creator } + + subject { described_class.new(project, user).execute } + + it { is_expected.to contain_exactly(state_1, state_2) } + + context 'user does not have permission' do + let(:user) { create(:user) } + + before do + project.add_guest(user) + end + + it { is_expected.to be_empty } + end + + context 'filtering by name' do + let(:params) { { name: name_param } } + + subject { described_class.new(project, user, params: params).execute } + + context 'name does not match' do + let(:name_param) { 'other-name' } + + it { is_expected.to be_empty } + end + + context 'name does match' do + let(:name_param) { state_1.name } + + it { is_expected.to contain_exactly(state_1) } + end + end + end +end diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb index ddba9b595a4..afebff5b5c9 100644 --- a/spec/finders/user_recent_events_finder_spec.rb +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -51,6 +51,20 @@ RSpec.describe UserRecentEventsFinder do end end + describe 'issue activity events' do + let(:issue) { create(:issue, project: public_project) } + let(:note) { create(:note_on_issue, noteable: issue, project: public_project) } + let!(:event_a) { create(:event, :commented, target: note, author: project_owner) } + let!(:event_b) { create(:event, :closed, target: issue, author: project_owner) } + + it 'includes all issue related events', :aggregate_failures do + events = finder.execute + + expect(events).to include(event_a) + expect(events).to include(event_b) + end + end + context 'limits' do before do stub_const("#{described_class}::DEFAULT_LIMIT", 1) diff --git a/spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json b/spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json new file mode 100644 index 00000000000..63e0c68e9cd --- /dev/null +++ b/spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "description": "The schema used to display codequality report in mr diff", + "required": ["files"], + "properties": { + "patternProperties": { + ".*.": { + "type": "array", + "items": { + "required": ["line", "description", "severity"], + "properties": { + "line": { "type": "integer" }, + "description": { "type": "string" }, + "severity": { "type": "string" } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/spec/fixtures/api/schemas/entities/group_group_link.json b/spec/fixtures/api/schemas/entities/group_group_link.json deleted file mode 100644 index bf94bbb3ce4..00000000000 --- a/spec/fixtures/api/schemas/entities/group_group_link.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "type": "object", - "required": [ - "id", - "created_at", - "expires_at", - "can_update", - "can_remove", - "access_level", - "valid_roles" - ], - "properties": { - "id": { "type": "integer" }, - "created_at": { "type": "date-time" }, - "expires_at": { "type": ["date-time", "null"] }, - "can_update": { "type": "boolean" }, - "can_remove": { "type": "boolean" }, - "access_level": { - "type": "object", - "required": ["integer_value", "string_value"], - "properties": { - "integer_value": { "type": "integer" }, - "string_value": { "type": "string" } - } - }, - "valid_roles": { "type": "object" }, - "shared_with_group": { - "type": "object", - "required": ["id", "name", "full_name", "full_path", "avatar_url", "web_url"], - "properties": { - "id": { "type": "integer" }, - "name": { "type": "string" }, - "full_name": { "type": "string" }, - "full_path": { "type": "string" }, - "avatar_url": { "type": ["string", "null"] }, - "web_url": { "type": "string" } - } - } - } -} diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json index e8b40745803..03b1872632e 100644 --- a/spec/fixtures/api/schemas/entities/member.json +++ b/spec/fixtures/api/schemas/entities/member.json @@ -9,7 +9,8 @@ "source", "valid_roles", "can_update", - "can_remove" + "can_remove", + "is_direct_member" ], "properties": { "id": { "type": "integer" }, @@ -18,6 +19,7 @@ "requested_at": { "type": ["date-time", "null"] }, "can_update": { "type": "boolean" }, "can_remove": { "type": "boolean" }, + "is_direct_member": { "type": "boolean" }, "access_level": { "type": "object", "required": ["integer_value", "string_value"], diff --git a/spec/fixtures/api/schemas/entities/member_user.json b/spec/fixtures/api/schemas/entities/member_user.json index 983cdb7b9d9..ebd26bfaaaa 100644 --- a/spec/fixtures/api/schemas/entities/member_user.json +++ b/spec/fixtures/api/schemas/entities/member_user.json @@ -9,6 +9,7 @@ "web_url": { "type": "string" }, "blocked": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean" }, + "availability": { "type": ["string", "null"] }, "status": { "type": "object", "required": ["emoji"], diff --git a/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json b/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json deleted file mode 100644 index bcf64a6e567..00000000000 --- a/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "object", - "allOf": [{ "$ref": "./package_details.json" }], - "properties": { - "target_sha": { - "type": "string" - }, - "composer_json": { - "type": "object" - } - } -} diff --git a/spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json b/spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json new file mode 100644 index 00000000000..db9b25889be --- /dev/null +++ b/spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "additionalProperties": false, + "required": ["targetSha", "composerJson"], + "properties": { + "targetSha": { + "type": "string" + }, + "composerJson": { + "type": "object", + "additionalProperties": false, + "required": ["name", "type", "license", "version"], + "properties": { + "name": { "type": "string" }, + "type": { "type": "string" }, + "license": { "type": "string" }, + "version": { "type": "string" } + } + } + } +} diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json index 4f90285183c..d2e2e65db54 100644 --- a/spec/fixtures/api/schemas/graphql/packages/package_details.json +++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json @@ -1,5 +1,10 @@ { "type": "object", + "additionalProperties": false, + "required": [ + "id", "name", "createdAt", "updatedAt", "version", "packageType", + "project", "tags", "pipelines", "versions", "metadata" + ], "properties": { "id": { "type": "string" @@ -16,21 +21,46 @@ "version": { "type": ["string", "null"] }, - "package_type": { + "packageType": { "type": ["string"], "enum": ["MAVEN", "NPM", "CONAN", "NUGET", "PYPI", "COMPOSER", "GENERIC", "GOLANG", "DEBIAN"] }, "tags": { - "type": "object" + "type": "object", + "additionalProperties": false, + "properties": { + "pageInfo": { "type": "object" }, + "edges": { "type": "array" }, + "nodes": { "type": "array" } + } }, "project": { "type": "object" }, "pipelines": { - "type": "object" + "type": "object", + "additionalProperties": false, + "properties": { + "pageInfo": { "type": "object" }, + "count": { "type": "integer" }, + "edges": { "type": "array" }, + "nodes": { "type": "array" } + } }, "versions": { - "type": "object" + "type": "object", + "additionalProperties": false, + "properties": { + "pageInfo": { "type": "object" }, + "edges": { "type": "array" }, + "nodes": { "type": "array" } + } + }, + "metadata": { + "anyOf": [ + { "$ref": "./package_composer_metadata.json" }, + { "type": "null" } + ] } } } diff --git a/spec/fixtures/api/schemas/group_group_links.json b/spec/fixtures/api/schemas/group_group_links.json deleted file mode 100644 index f8b4e7f035b..00000000000 --- a/spec/fixtures/api/schemas/group_group_links.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "array", - "items": { - "$ref": "entities/group_group_link.json" - } -} diff --git a/spec/fixtures/api/schemas/group_link/group_group_link.json b/spec/fixtures/api/schemas/group_link/group_group_link.json new file mode 100644 index 00000000000..bfca5c885e3 --- /dev/null +++ b/spec/fixtures/api/schemas/group_link/group_group_link.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "group_link.json" }, + { + "required": [ + "can_update", + "can_remove" + ], + "properties": { + "can_update": { "type": "boolean" }, + "can_remove": { "type": "boolean" } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/group_link/group_group_links.json b/spec/fixtures/api/schemas/group_link/group_group_links.json new file mode 100644 index 00000000000..2c0bf20f524 --- /dev/null +++ b/spec/fixtures/api/schemas/group_link/group_group_links.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "group_group_link.json" + } +} diff --git a/spec/fixtures/api/schemas/group_link/group_link.json b/spec/fixtures/api/schemas/group_link/group_link.json new file mode 100644 index 00000000000..300790728a8 --- /dev/null +++ b/spec/fixtures/api/schemas/group_link/group_link.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "required": [ + "id", + "created_at", + "expires_at", + "access_level", + "valid_roles" + ], + "properties": { + "id": { "type": "integer" }, + "created_at": { "type": "date-time" }, + "expires_at": { "type": ["date-time", "null"] }, + "access_level": { + "type": "object", + "required": ["integer_value", "string_value"], + "properties": { + "integer_value": { "type": "integer" }, + "string_value": { "type": "string" } + }, + "additionalProperties": false + }, + "valid_roles": { "type": "object" }, + "shared_with_group": { + "type": "object", + "required": ["id", "name", "full_name", "full_path", "avatar_url", "web_url"], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "full_name": { "type": "string" }, + "full_path": { "type": "string" }, + "avatar_url": { "type": ["string", "null"] }, + "web_url": { "type": "string" } + }, + "additionalProperties": false + } + } +} diff --git a/spec/fixtures/api/schemas/group_link/project_group_link.json b/spec/fixtures/api/schemas/group_link/project_group_link.json new file mode 100644 index 00000000000..bfca5c885e3 --- /dev/null +++ b/spec/fixtures/api/schemas/group_link/project_group_link.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "group_link.json" }, + { + "required": [ + "can_update", + "can_remove" + ], + "properties": { + "can_update": { "type": "boolean" }, + "can_remove": { "type": "boolean" } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/group_link/project_group_links.json b/spec/fixtures/api/schemas/group_link/project_group_links.json new file mode 100644 index 00000000000..fc024d67f36 --- /dev/null +++ b/spec/fixtures/api/schemas/group_link/project_group_links.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "project_group_link.json" + } +} diff --git a/spec/fixtures/dependency_proxy/manifest b/spec/fixtures/dependency_proxy/manifest index a899d05d697..ed543883d60 100644 --- a/spec/fixtures/dependency_proxy/manifest +++ b/spec/fixtures/dependency_proxy/manifest @@ -1,38 +1,16 @@ { - "schemaVersion": 1, - "name": "library/alpine", - "tag": "latest", - "architecture": "amd64", - "fsLayers": [ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1472, + "digest": "sha256:7731472c3f2a25edbb9c085c78f42ec71259f2b83485aa60648276d408865839" + }, + "layers": [ { - "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" - }, - { - "blobSum": "sha256:188c0c94c7c576fff0792aca7ec73d67a2f7f4cb3a6e53a84559337260b36964" - } - ], - "history": [ - { - "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"8c59eb170e19b8c3768b8d06c91053b0debf4a6fa6a452df394145fe9b885ea5\",\"container_config\":{\"Hostname\":\"8c59eb170e19\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"/bin/sh\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2020-10-22T02:19:24.499382102Z\",\"docker_version\":\"18.09.7\",\"id\":\"c5f1aab5bb88eaf1aa62bea08ea6654547d43fd4d15b1a476c77e705dd5385ba\",\"os\":\"linux\",\"parent\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"throwaway\":true}" - }, - { - "v1Compatibility": "{\"id\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"created\":\"2020-10-22T02:19:24.33416307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:f17f65714f703db9012f00e5ec98d0b2541ff6147c2633f7ab9ba659d0c507f4 in / \"]}}" - } - ], - "signatures": [ - { - "header": { - "jwk": { - "crv": "P-256", - "kid": "XOTE:DZ4C:YBPJ:3O3L:YI4B:NYXU:T4VR:USH6:CXXN:SELU:CSCC:FVPE", - "kty": "EC", - "x": "cR1zye_3354mdbD7Dn-mtXNXvtPtmLlUVDa5vH6Lp74", - "y": "rldUXSllLit6_2BW6AV8aqkwWJXHoYPG9OwkIBouwxQ" - }, - "alg": "ES256" - }, - "signature": "DYB2iB-XKIisqp5Q0OXFOBIOlBOuRV7pnZuKy0cxVB2Qj1VFRhWX4Tq336y0VMWbF6ma1he5A1E_Vk4jazrJ9g", - "protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0xMS0yNFQyMjowMTo1MVoifQ" + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2810825, + "digest": "sha256:596ba82af5aaa3e2fd9d6f955b8b94f0744a2b60710e3c243ba3e4a467f051d1" } ] } \ No newline at end of file diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index aff4b1aae23..30ae58dbf9e 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -170,6 +170,8 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Ignores invalid: <%= User.reference_prefix %>fake_user - Ignored in code: `<%= user.to_reference %>` - Ignored in links: [Link to <%= user.to_reference %>](#user-link) +- Ignored when backslash escaped: \<%= user.to_reference %> +- Ignored when backslash escaped: \<%= group.to_reference %> - Link to user by reference: [User](<%= user.to_reference %>) #### IssueReferenceFilter @@ -178,6 +180,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Issue in another project: <%= xissue.to_reference(project) %> - Ignored in code: `<%= issue.to_reference %>` - Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) +- Ignored when backslash escaped: \<%= issue.to_reference %> - Issue by URL: <%= urls.project_issue_url(issue.project, issue) %> - Link to issue by reference: [Issue](<%= issue.to_reference %>) - Link to issue by URL: [Issue](<%= urls.project_issue_url(issue.project, issue) %>) @@ -188,6 +191,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Merge request in another project: <%= xmerge_request.to_reference(project) %> - Ignored in code: `<%= merge_request.to_reference %>` - Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link) +- Ignored when backslash escaped: \<%= merge_request.to_reference %> - Merge request by URL: <%= urls.project_merge_request_url(merge_request.project, merge_request) %> - Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>) - Link to merge request by URL: [Merge request](<%= urls.project_merge_request_url(merge_request.project, merge_request) %>) @@ -198,6 +202,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Snippet in another project: <%= xsnippet.to_reference(project) %> - Ignored in code: `<%= snippet.to_reference %>` - Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) +- Ignored when backslash escaped: \<%= snippet.to_reference %> - Snippet by URL: <%= urls.project_snippet_url(snippet.project, snippet) %> - Link to snippet by reference: [Snippet](<%= snippet.to_reference %>) - Link to snippet by URL: [Snippet](<%= urls.project_snippet_url(snippet.project, snippet) %>) @@ -229,6 +234,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Label by name in quotes: <%= label.to_reference(format: :name) %> - Ignored in code: `<%= simple_label.to_reference %>` - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) +- Ignored when backslash escaped: \<%= simple_label.to_reference %> - Link to label by reference: [Label](<%= label.to_reference %>) #### MilestoneReferenceFilter @@ -239,6 +245,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Milestone in another project: <%= xmilestone.to_reference(project) %> - Ignored in code: `<%= simple_milestone.to_reference %>` - Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link) +- Ignored when backslash escaped: \<%= simple_milestone.to_reference %> - Milestone by URL: <%= urls.milestone_url(milestone) %> - Link to milestone by URL: [Milestone](<%= milestone.to_reference %>) - Group milestone by name: <%= Milestone.reference_prefix %><%= group_milestone.name %> @@ -250,6 +257,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Alert in another project: <%= xalert.to_reference(project) %> - Ignored in code: `<%= alert.to_reference %>` - Ignored in links: [Link to <%= alert.to_reference %>](#alert-link) +- Ignored when backslash escaped: \<%= alert.to_reference %> - Alert by URL: <%= alert.details_url %> - Link to alert by reference: [Alert](<%= alert.to_reference %>) - Link to alert by URL: [Alert](<%= alert.details_url %>) diff --git a/spec/fixtures/packages/composer/package.json b/spec/fixtures/packages/composer/package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/spec/fixtures/packages/composer/package.json @@ -0,0 +1 @@ +{} diff --git a/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json b/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json new file mode 100644 index 00000000000..c3ee2bc4cac --- /dev/null +++ b/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json @@ -0,0 +1,23 @@ +{ + "files": { + "file_a.rb": [ + { + "line": 10, + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "severity": "major" + }, + { + "line": 10, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "severity": "minor" + } + ], + "file_b.rb": [ + { + "line": 10, + "description": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count.", + "severity": "minor" + } + ] + } +} diff --git a/spec/fixtures/whats_new/invalid.yml b/spec/fixtures/whats_new/invalid.yml index 0e588efaf8f..a3342be0f24 100644 --- a/spec/fixtures/whats_new/invalid.yml +++ b/spec/fixtures/whats_new/invalid.yml @@ -13,7 +13,7 @@ stage: Release self-managed: true gitlab-com: true - packages: [Starter] + packages: [Free] url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png published_at: 2020-04-22 diff --git a/spec/fixtures/whats_new/valid.yml b/spec/fixtures/whats_new/valid.yml index cbe9d666357..ec465f47989 100644 --- a/spec/fixtures/whats_new/valid.yml +++ b/spec/fixtures/whats_new/valid.yml @@ -13,7 +13,7 @@ stage: Release self-managed: true gitlab-com: true - packages: [Starter] + packages: [Free] url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png published_at: 2020-04-22 diff --git a/spec/frontend/__helpers__/graphql_helpers.js b/spec/frontend/__helpers__/graphql_helpers.js new file mode 100644 index 00000000000..63123aa046f --- /dev/null +++ b/spec/frontend/__helpers__/graphql_helpers.js @@ -0,0 +1,14 @@ +/** + * Returns a clone of the given object with all __typename keys omitted, + * including deeply nested ones. + * + * Only works with JSON-serializable objects. + * + * @param {object} An object with __typename keys (e.g., a GraphQL response) + * @returns {object} A new object with no __typename keys + */ +export const stripTypenames = (object) => { + return JSON.parse( + JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)), + ); +}; diff --git a/spec/frontend/__helpers__/graphql_helpers_spec.js b/spec/frontend/__helpers__/graphql_helpers_spec.js new file mode 100644 index 00000000000..dd23fbbf4e9 --- /dev/null +++ b/spec/frontend/__helpers__/graphql_helpers_spec.js @@ -0,0 +1,23 @@ +import { stripTypenames } from './graphql_helpers'; + +describe('stripTypenames', () => { + it.each` + input | expected + ${{}} | ${{}} + ${{ __typename: 'Foo' }} | ${{}} + ${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }} + ${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }} + ${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }} + ${[]} | ${[]} + ${[{ __typename: 'Foo' }]} | ${[{}]} + ${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]} + `('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => { + const actual = stripTypenames(input); + expect(actual).toEqual(expected); + expect(input).not.toBe(actual); + }); + + it('given null returns null', () => { + expect(stripTypenames(null)).toEqual(null); + }); +}); diff --git a/spec/frontend/__helpers__/stub_component.js b/spec/frontend/__helpers__/stub_component.js index 45550450517..96fe3a8bc45 100644 --- a/spec/frontend/__helpers__/stub_component.js +++ b/spec/frontend/__helpers__/stub_component.js @@ -1,7 +1,32 @@ +/** + * Returns a new object with keys pointing to stubbed methods + * + * This is helpful for stubbing components like GlModal where it's supported + * in the API to call `.show()` and `.hide()` ([Bootstrap Vue docs][1]). + * + * [1]: https://bootstrap-vue.org/docs/components/modal#using-show-hide-and-toggle-component-methods + * + * @param {Object} methods - Object whose keys will be in the returned object. + */ +const createStubbedMethods = (methods = {}) => { + if (!methods) { + return {}; + } + + return Object.keys(methods).reduce( + (acc, key) => + Object.assign(acc, { + [key]: () => {}, + }), + {}, + ); +}; + export function stubComponent(Component, options = {}) { return { props: Component.props, model: Component.model, + methods: createStubbedMethods(Component.methods), // Do not render any slots/scoped slots except default // This differs from VTU behavior which renders all slots template: '
', 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 1a3b151afa0..0faf0db4135 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 @@ -2,11 +2,11 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import { GlModal, GlSearchBoxByType } from '@gitlab/ui'; import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue'; -import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit'; import defaultState from '~/add_context_commits_modal/store/state'; import mutations from '~/add_context_commits_modal/store/mutations'; import * as actions from '~/add_context_commits_modal/store/actions'; +import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit'; const localVue = createLocalVue(); localVue.use(Vuex); diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js new file mode 100644 index 00000000000..78bc37233c2 --- /dev/null +++ b/spec/frontend/admin/users/components/user_actions_spec.js @@ -0,0 +1,138 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlDropdownDivider } from '@gitlab/ui'; +import AdminUserActions from '~/admin/users/components/user_actions.vue'; +import { generateUserPaths } from '~/admin/users/utils'; + +import { users, paths } from '../mock_data'; + +const BLOCK = 'block'; +const EDIT = 'edit'; +const LDAP = 'ldapBlocked'; +const DELETE = 'delete'; +const DELETE_WITH_CONTRIBUTIONS = 'deleteWithContributions'; + +describe('AdminUserActions component', () => { + let wrapper; + const user = users[0]; + const userPaths = generateUserPaths(paths, user.username); + + const findEditButton = () => wrapper.find('[data-testid="edit"]'); + const findActionsDropdown = () => wrapper.find('[data-testid="actions"'); + const findDropdownDivider = () => wrapper.find(GlDropdownDivider); + + const initComponent = ({ actions = [] } = {}) => { + wrapper = shallowMount(AdminUserActions, { + propsData: { + user: { + ...user, + actions, + }, + paths, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('edit button', () => { + describe('when the user has an edit action attached', () => { + beforeEach(() => { + initComponent({ actions: [EDIT] }); + }); + + it('renders the edit button linking to the user edit path', () => { + expect(findEditButton().exists()).toBe(true); + expect(findEditButton().attributes('href')).toBe(userPaths.edit); + }); + }); + + describe('when there is no edit action attached to the user', () => { + beforeEach(() => { + initComponent({ actions: [] }); + }); + + it('does not render the edit button linking to the user edit path', () => { + expect(findEditButton().exists()).toBe(false); + }); + }); + }); + + describe('actions dropdown', () => { + describe('when there are actions', () => { + const actions = [EDIT, BLOCK]; + + beforeEach(() => { + initComponent({ actions }); + }); + + it('renders the actions dropdown', () => { + expect(findActionsDropdown().exists()).toBe(true); + }); + + it.each(actions)('renders a dropdown item for %s', (action) => { + const dropdownAction = wrapper.find(`[data-testid="${action}"]`); + expect(dropdownAction.exists()).toBe(true); + expect(dropdownAction.attributes('href')).toBe(userPaths[action]); + }); + + describe('when there is a LDAP action', () => { + beforeEach(() => { + initComponent({ actions: [LDAP] }); + }); + + it('renders the LDAP dropdown item without a link', () => { + const dropdownAction = wrapper.find(`[data-testid="${LDAP}"]`); + expect(dropdownAction.exists()).toBe(true); + expect(dropdownAction.attributes('href')).toBe(undefined); + }); + }); + + describe('when there is a delete action', () => { + const deleteActions = [DELETE, DELETE_WITH_CONTRIBUTIONS]; + + beforeEach(() => { + initComponent({ actions: [BLOCK, ...deleteActions] }); + }); + + it('renders a dropdown divider', () => { + expect(findDropdownDivider().exists()).toBe(true); + }); + + it('only renders delete dropdown items for actions containing the word "delete"', () => { + const { length } = wrapper.findAll(`[data-testid*="delete-"]`); + expect(length).toBe(deleteActions.length); + }); + + it.each(deleteActions)('renders a delete dropdown item for %s', (action) => { + const deleteAction = wrapper.find(`[data-testid="delete-${action}"]`); + expect(deleteAction.exists()).toBe(true); + expect(deleteAction.attributes('href')).toBe(userPaths[action]); + }); + }); + + describe('when there are no delete actions', () => { + it('does not render a dropdown divider', () => { + expect(findDropdownDivider().exists()).toBe(false); + }); + + it('does not render a delete dropdown item', () => { + const anyDeleteAction = wrapper.find(`[data-testid*="delete-"]`); + expect(anyDeleteAction.exists()).toBe(false); + }); + }); + }); + + describe('when there are no actions', () => { + beforeEach(() => { + initComponent({ actions: [] }); + }); + + it('does not render the actions dropdown', () => { + expect(findActionsDropdown().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js index ba4e83690d0..0c048a06a34 100644 --- a/spec/frontend/admin/users/components/user_avatar_spec.js +++ b/spec/frontend/admin/users/components/user_avatar_spec.js @@ -1,7 +1,10 @@ -import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlAvatarLink, GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import AdminUserAvatar from '~/admin/users/components/user_avatar.vue'; +import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants'; +import { truncate } from '~/lib/utils/text_utility'; import { users, paths } from '../mock_data'; describe('AdminUserAvatar component', () => { @@ -9,17 +12,25 @@ describe('AdminUserAvatar component', () => { const user = users[0]; const adminUserPath = paths.adminUser; + const findNote = () => wrapper.find(GlIcon); const findAvatar = () => wrapper.find(GlAvatarLabeled); const findAvatarLink = () => wrapper.find(GlAvatarLink); const findAllBadges = () => wrapper.findAll(GlBadge); + const findTooltip = () => getBinding(findNote().element, 'gl-tooltip'); const initComponent = (props = {}) => { - wrapper = mount(AdminUserAvatar, { + wrapper = shallowMount(AdminUserAvatar, { propsData: { user, adminUserPath, ...props, }, + directives: { + GlTooltip: createMockDirective(), + }, + stubs: { + GlAvatarLabeled, + }, }); }; @@ -53,11 +64,58 @@ describe('AdminUserAvatar component', () => { expect(findAvatar().attributes('src')).toBe(user.avatarUrl); }); + it('renders a user note icon', () => { + expect(findNote().exists()).toBe(true); + expect(findNote().props('name')).toBe('document'); + }); + + it("renders the user's note tooltip", () => { + const tooltip = findTooltip(); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(user.note); + }); + it("renders the user's badges", () => { findAllBadges().wrappers.forEach((badge, idx) => { expect(badge.text()).toBe(user.badges[idx].text); expect(badge.props('variant')).toBe(user.badges[idx].variant); }); }); + + describe('and the user note is very long', () => { + const noteText = new Array(LENGTH_OF_USER_NOTE_TOOLTIP + 1).join('a'); + + beforeEach(() => { + initComponent({ + user: { + ...user, + note: noteText, + }, + }); + }); + + it("renders a truncated user's note tooltip", () => { + const tooltip = findTooltip(); + + expect(tooltip).toBeDefined(); + expect(tooltip.value).toBe(truncate(noteText, LENGTH_OF_USER_NOTE_TOOLTIP)); + }); + }); + + describe('and the user does not have a note', () => { + beforeEach(() => { + initComponent({ + user: { + ...user, + note: null, + }, + }); + }); + + it('does not render a user note', () => { + expect(findNote().exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js new file mode 100644 index 00000000000..6428b10059b --- /dev/null +++ b/spec/frontend/admin/users/components/user_date_spec.js @@ -0,0 +1,34 @@ +import { shallowMount } from '@vue/test-utils'; + +import UserDate from '~/admin/users/components/user_date.vue'; +import { users } from '../mock_data'; + +const mockDate = users[0].createdAt; + +describe('FormatDate component', () => { + let wrapper; + + const initComponent = (props = {}) => { + wrapper = shallowMount(UserDate, { + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each` + date | output + ${mockDate} | ${'13 Nov, 2020'} + ${null} | ${'Never'} + ${undefined} | ${'Never'} + `('renders $date as $output', ({ date, output }) => { + initComponent({ date }); + + expect(wrapper.text()).toBe(output); + }); +}); diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js index b79d2d4d39d..ac48542cec5 100644 --- a/spec/frontend/admin/users/components/users_table_spec.js +++ b/spec/frontend/admin/users/components/users_table_spec.js @@ -3,6 +3,9 @@ import { mount } from '@vue/test-utils'; import AdminUsersTable from '~/admin/users/components/users_table.vue'; import AdminUserAvatar from '~/admin/users/components/user_avatar.vue'; +import AdminUserDate from '~/admin/users/components/user_date.vue'; +import AdminUserActions from '~/admin/users/components/user_actions.vue'; + import { users, paths } from '../mock_data'; describe('AdminUsersTable component', () => { @@ -39,18 +42,21 @@ describe('AdminUsersTable component', () => { initComponent(); }); - it.each` - key | label - ${'name'} | ${'Name'} - ${'projectsCount'} | ${'Projects'} - ${'createdAt'} | ${'Created on'} - ${'lastActivityOn'} | ${'Last activity'} - `('renders users.$key in column $label', ({ key, label }) => { - expect(getCellByLabel(0, label).text()).toContain(`${user[key]}`); + it('renders the projects count', () => { + expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`); }); - it('renders an AdminUserAvatar component', () => { - expect(getCellByLabel(0, 'Name').find(AdminUserAvatar).exists()).toBe(true); + it('renders the user actions', () => { + expect(wrapper.find(AdminUserActions).exists()).toBe(true); + }); + + it.each` + component | label + ${AdminUserAvatar} | ${'Name'} + ${AdminUserDate} | ${'Created on'} + ${AdminUserDate} | ${'Last activity'} + `('renders the component for column $label', ({ component, label }) => { + expect(getCellByLabel(0, label).find(component).exists()).toBe(true); }); }); diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js index 171d54c8f4f..20b60bd8640 100644 --- a/spec/frontend/admin/users/index_spec.js +++ b/spec/frontend/admin/users/index_spec.js @@ -1,5 +1,5 @@ import { createWrapper } from '@vue/test-utils'; -import initAdminUsers from '~/admin/users'; +import { initAdminUsersApp } from '~/admin/users'; import AdminUsersApp from '~/admin/users/components/app.vue'; import { users, paths } from './mock_data'; @@ -16,7 +16,7 @@ describe('initAdminUsersApp', () => { document.body.appendChild(el); - wrapper = createWrapper(initAdminUsers(el)); + wrapper = createWrapper(initAdminUsersApp(el)); }); afterEach(() => { diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js index 860994a9152..c3918ef5173 100644 --- a/spec/frontend/admin/users/mock_data.js +++ b/spec/frontend/admin/users/mock_data.js @@ -14,6 +14,7 @@ export const users = [ ], projectsCount: 0, actions: [], + note: 'Create per issue #999', }, ]; diff --git a/spec/frontend/alert_management/components/alert_details_spec.js b/spec/frontend/alert_management/components/alert_details_spec.js deleted file mode 100644 index 976e50625a6..00000000000 --- a/spec/frontend/alert_management/components/alert_details_spec.js +++ /dev/null @@ -1,338 +0,0 @@ -import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import AlertDetails from '~/alert_management/components/alert_details.vue'; -import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue'; -import { - ALERTS_SEVERITY_LABELS, - trackAlertsDetailsViewsOptions, -} from '~/alert_management/constants'; -import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql'; -import { joinPaths } from '~/lib/utils/url_utility'; -import Tracking from '~/tracking'; -import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; -const environmentName = 'Production'; -const environmentPath = '/fake/path'; - -describe('AlertDetails', () => { - let environmentData = { name: environmentName, path: environmentPath }; - let mock; - let wrapper; - const projectPath = 'root/alerts'; - const projectIssuesPath = 'root/alerts/-/issues'; - const projectId = '1'; - const $router = { replace: jest.fn() }; - - function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) { - wrapper = extendedWrapper( - mountMethod(AlertDetails, { - provide: { - alertId: 'alertId', - projectPath, - projectIssuesPath, - projectId, - }, - data() { - return { - alert: { - ...mockAlert, - environment: environmentData, - }, - sidebarStatus: false, - ...data, - }; - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - sidebarStatus: {}, - }, - }, - $router, - $route: { params: {} }, - }, - stubs: { - ...stubs, - AlertSummaryRow, - }, - }), - ); - } - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - mock.restore(); - }); - - const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn'); - const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn'); - const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError'); - const findEnvironmentName = () => wrapper.findByTestId('environmentName'); - const findEnvironmentPath = () => wrapper.findByTestId('environmentPath'); - const findDetailsTable = () => wrapper.find(AlertDetailsTable); - - describe('Alert details', () => { - describe('when alert is null', () => { - beforeEach(() => { - mountComponent({ data: { alert: null } }); - }); - - it('shows an empty state', () => { - expect(wrapper.findByTestId('alertDetailsTabs').exists()).toBe(false); - }); - }); - - describe('when alert is present', () => { - beforeEach(() => { - mountComponent({ data: { alert: mockAlert } }); - }); - - it('renders a tab with overview information', () => { - expect(wrapper.findByTestId('overview').exists()).toBe(true); - }); - - it('renders a tab with an activity feed', () => { - expect(wrapper.findByTestId('activity').exists()).toBe(true); - }); - - it('renders severity', () => { - expect(wrapper.findByTestId('severity').text()).toBe( - ALERTS_SEVERITY_LABELS[mockAlert.severity], - ); - }); - - it('renders a title', () => { - expect(wrapper.findByTestId('title').text()).toBe(mockAlert.title); - }); - - it('renders a start time', () => { - expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true); - expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt); - }); - }); - - describe('individual alert fields', () => { - describe.each` - field | data | isShown - ${'eventCount'} | ${1} | ${true} - ${'eventCount'} | ${undefined} | ${false} - ${'monitoringTool'} | ${'New Relic'} | ${true} - ${'monitoringTool'} | ${undefined} | ${false} - ${'service'} | ${'Prometheus'} | ${true} - ${'service'} | ${undefined} | ${false} - ${'runbook'} | ${undefined} | ${false} - ${'runbook'} | ${'run.com'} | ${true} - `(`$desc`, ({ field, data, isShown }) => { - beforeEach(() => { - mountComponent({ data: { alert: { ...mockAlert, [field]: data } } }); - }); - - it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => { - const element = wrapper.findByTestId(field); - if (isShown) { - expect(element.text()).toContain(data.toString()); - } else { - expect(wrapper.findByTestId(field).exists()).toBe(false); - } - }); - }); - }); - - describe('environment fields', () => { - it('should show the environment name with a link to the path', () => { - mountComponent(); - const path = findEnvironmentPath(); - - expect(findEnvironmentName().exists()).toBe(false); - expect(path.text()).toBe(environmentName); - expect(path.attributes('href')).toBe(environmentPath); - }); - - it('should only show the environment name if the path is not provided', () => { - environmentData = { name: environmentName, path: null }; - mountComponent(); - - expect(findEnvironmentPath().exists()).toBe(false); - expect(findEnvironmentName().text()).toBe(environmentName); - }); - }); - - describe('Create incident from alert', () => { - it('should display "View incident" button that links the incident page when incident exists', () => { - const issueIid = '3'; - mountComponent({ - data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false }, - }); - - expect(findViewIncidentBtn().exists()).toBe(true); - expect(findViewIncidentBtn().attributes('href')).toBe( - joinPaths(projectIssuesPath, issueIid), - ); - expect(findCreateIncidentBtn().exists()).toBe(false); - }); - - it('should display "Create incident" button when incident doesn\'t exist yet', () => { - const issueIid = null; - mountComponent({ - mountMethod: mount, - data: { alert: { ...mockAlert, issueIid } }, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(findViewIncidentBtn().exists()).toBe(false); - expect(findCreateIncidentBtn().exists()).toBe(true); - }); - }); - - it('calls `$apollo.mutate` with `createIssueQuery`', () => { - const issueIid = '10'; - mountComponent({ - mountMethod: mount, - data: { alert: { ...mockAlert } }, - }); - - jest - .spyOn(wrapper.vm.$apollo, 'mutate') - .mockResolvedValue({ data: { createAlertIssue: { issue: { iid: issueIid } } } }); - findCreateIncidentBtn().trigger('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: createIssueMutation, - variables: { - iid: mockAlert.iid, - projectPath, - }, - }); - }); - - it('shows error alert when incident creation fails ', async () => { - const errorMsg = 'Something went wrong'; - mountComponent({ - mountMethod: mount, - data: { alert: { ...mockAlert, alertIid: 1 } }, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); - findCreateIncidentBtn().trigger('click'); - - await waitForPromises(); - expect(findIncidentCreationAlert().text()).toBe(errorMsg); - }); - }); - - describe('View full alert details', () => { - beforeEach(() => { - mountComponent({ data: { alert: mockAlert } }); - }); - - it('should display a table of raw alert details data', () => { - expect(findDetailsTable().exists()).toBe(true); - }); - }); - - describe('loading state', () => { - beforeEach(() => { - mountComponent({ loading: true }); - }); - - it('displays a loading state when loading', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - }); - }); - - describe('error state', () => { - it('displays a error state correctly', () => { - mountComponent({ data: { errored: true } }); - expect(wrapper.find(GlAlert).exists()).toBe(true); - }); - - it('renders html-errors correctly', () => { - mountComponent({ - data: { errored: true, sidebarErrorMessage: '' }, - }); - expect(wrapper.findByTestId('htmlError').exists()).toBe(true); - }); - - it('does not display an error when dismissed', () => { - mountComponent({ data: { errored: true, isErrorDismissed: true } }); - expect(wrapper.find(GlAlert).exists()).toBe(false); - }); - }); - - describe('header', () => { - const findHeader = () => wrapper.findByTestId('alert-header'); - const stubs = { TimeAgoTooltip: { template: 'now' } }; - - describe('individual header fields', () => { - describe.each` - createdAt | monitoringTool | result - ${'2020-04-17T23:18:14.996Z'} | ${null} | ${'Alert Reported now'} - ${'2020-04-17T23:18:14.996Z'} | ${'Datadog'} | ${'Alert Reported now by Datadog'} - `( - `When createdAt=$createdAt, monitoringTool=$monitoringTool`, - ({ createdAt, monitoringTool, result }) => { - beforeEach(() => { - mountComponent({ - data: { alert: { ...mockAlert, createdAt, monitoringTool } }, - mountMethod: mount, - stubs, - }); - }); - - it('header text is shown correctly', () => { - expect(findHeader().text()).toBe(result); - }); - }, - ); - }); - }); - - describe('tab navigation', () => { - beforeEach(() => { - mountComponent({ data: { alert: mockAlert } }); - }); - - it.each` - index | tabId - ${0} | ${'overview'} - ${1} | ${'metrics'} - ${2} | ${'activity'} - `('will navigate to the correct tab via $tabId', ({ index, tabId }) => { - wrapper.setData({ currentTabIndex: index }); - expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } }); - }); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alert: mockAlert }, - loading: false, - }); - }); - - it('should track alert details page views', () => { - const { category, action } = trackAlertsDetailsViewsOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js deleted file mode 100644 index ea7b4584a63..00000000000 --- a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js +++ /dev/null @@ -1,112 +0,0 @@ -import { mount } from '@vue/test-utils'; -import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue'; -import createAlertTodoMutation from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql'; -import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar To Do', () => { - let wrapper; - - function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { - wrapper = mount(SidebarTodo, { - propsData: { - alert: { ...mockAlert }, - ...data, - sidebarCollapsed, - projectPath: 'projectPath', - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - afterEach(() => { - wrapper.destroy(); - }); - - 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({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - }); - }); - - it('renders a button for adding a To-Do', async () => { - await wrapper.vm.$nextTick(); - - expect(findToDoButton().text()).toBe('Add a 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 wrapper.vm.$nextTick(); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: createAlertTodoMutation, - variables: { - iid: '1527542', - projectPath: 'projectPath', - }, - }); - }); - }); - - describe('removing a todo', () => { - beforeEach(() => { - mountComponent({ - data: { alert: { ...mockAlert, todos: { nodes: [{ id: '1234' }] } } }, - sidebarCollapsed: false, - loading: false, - }); - }); - - it('renders a Mark As Done button when todo is present', async () => { - await wrapper.vm.$nextTick(); - - expect(findToDoButton().text()).toBe('Mark as done'); - }); - - it('calls `$apollo.mutate` with `todoMarkDoneMutation` mutation and variables containing `id`', async () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - - findToDoButton().trigger('click'); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: todoMarkDoneMutation, - update: expect.anything(), - variables: { - id: '1234', - }, - }); - }); - }); - }); -}); 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 0cc3d565e10..21217e6c608 100644 --- a/spec/frontend/alert_management/components/alert_management_table_spec.js +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -2,11 +2,11 @@ import { mount } from '@vue/test-utils'; import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@gitlab/ui'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json'; import { visitUrl } from '~/lib/utils/url_utility'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import AlertManagementTable from '~/alert_management/components/alert_management_table.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import mockAlerts from '../mocks/alerts.json'; import defaultProvideValues from '../mocks/alerts_provide_config.json'; jest.mock('~/lib/utils/url_utility', () => ({ diff --git a/spec/frontend/alert_management/components/alert_metrics_spec.js b/spec/frontend/alert_management/components/alert_metrics_spec.js deleted file mode 100644 index 42da8c3768b..00000000000 --- a/spec/frontend/alert_management/components/alert_metrics_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import waitForPromises from 'helpers/wait_for_promises'; -import MockAdapter from 'axios-mock-adapter'; -import axios from 'axios'; -import AlertMetrics from '~/alert_management/components/alert_metrics.vue'; -import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue'; - -jest.mock('~/monitoring/stores', () => ({ - monitoringDashboard: {}, -})); - -jest.mock('~/monitoring/components/embeds/metric_embed.vue', () => ({ - render(h) { - return h('div'); - }, -})); - -describe('Alert Metrics', () => { - let wrapper; - const mock = new MockAdapter(axios); - - function mountComponent({ props } = {}) { - wrapper = shallowMount(AlertMetrics, { - propsData: { - ...props, - }, - }); - } - - const findChart = () => wrapper.find(MetricEmbed); - const findEmptyState = () => wrapper.find({ ref: 'emptyState' }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - afterAll(() => { - mock.restore(); - }); - - describe('Empty state', () => { - it('should display a message when metrics dashboard url is not provided ', () => { - mountComponent(); - expect(findChart().exists()).toBe(false); - expect(findEmptyState().text()).toBe("Metrics weren't available in the alerts payload."); - }); - }); - - describe('Chart', () => { - it('should be rendered when dashboard url is provided', async () => { - mountComponent({ props: { dashboardUrl: 'metrics.url' } }); - - await waitForPromises(); - await wrapper.vm.$nextTick(); - - expect(findEmptyState().exists()).toBe(false); - expect(findChart().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_status_spec.js b/spec/frontend/alert_management/components/alert_status_spec.js deleted file mode 100644 index 6f2ddb86020..00000000000 --- a/spec/frontend/alert_management/components/alert_status_spec.js +++ /dev/null @@ -1,151 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import waitForPromises from 'helpers/wait_for_promises'; -import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; -import AlertManagementStatus from '~/alert_management/components/alert_status.vue'; -import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql'; -import Tracking from '~/tracking'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('AlertManagementStatus', () => { - let wrapper; - const findStatusDropdown = () => wrapper.find(GlDropdown); - const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); - - const selectFirstStatusOption = () => { - findFirstStatusOption().vm.$emit('click'); - - return waitForPromises(); - }; - - function mountComponent({ props = {}, loading = false, stubs = {} } = {}) { - wrapper = shallowMount(AlertManagementStatus, { - propsData: { - alert: { ...mockAlert }, - projectPath: 'gitlab-org/gitlab', - isSidebar: false, - ...props, - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - beforeEach(() => { - mountComponent(); - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - describe('updating the alert status', () => { - const iid = '1527542'; - const mockUpdatedMutationResult = { - data: { - updateAlertStatus: { - errors: [], - alert: { - iid, - status: 'acknowledged', - }, - }, - }, - }; - - beforeEach(() => { - mountComponent({}); - }); - - it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - findFirstStatusOption().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateAlertStatusMutation, - variables: { - iid, - status: 'TRIGGERED', - projectPath: 'gitlab-org/gitlab', - }, - }); - }); - - describe('when a request fails', () => { - beforeEach(() => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - }); - - it('emits an error', async () => { - await selectFirstStatusOption(); - - expect(wrapper.emitted('alert-error')[0]).toEqual([ - 'There was an error while updating the status of the alert. Please try again.', - ]); - }); - - it('emits an error when triggered a second time', async () => { - await selectFirstStatusOption(); - await wrapper.vm.$nextTick(); - await selectFirstStatusOption(); - // Should emit two errors [0,1] - expect(wrapper.emitted('alert-error').length > 1).toBe(true); - }); - }); - - it('shows an error when response includes HTML errors', async () => { - const mockUpdatedMutationErrorResult = { - data: { - updateAlertStatus: { - errors: [''], - alert: { - iid, - status: 'acknowledged', - }, - }, - }, - }; - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult); - - await selectFirstStatusOption(); - - expect(wrapper.emitted('alert-error').length > 0).toBe(true); - expect(wrapper.emitted('alert-error')[0]).toEqual([ - 'There was an error while updating the status of the alert. ', - ]); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent({}); - }); - - it('should track alert status updates', () => { - Tracking.event.mockClear(); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); - findFirstStatusOption().vm.$emit('click'); - const status = findFirstStatusOption().text(); - setImmediate(() => { - const { category, action, label } = trackAlertStatusUpdateOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status }); - }); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_summary_row_spec.js b/spec/frontend/alert_management/components/alert_summary_row_spec.js deleted file mode 100644 index 47c715c089a..00000000000 --- a/spec/frontend/alert_management/components/alert_summary_row_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue'; - -const label = 'a label'; -const value = 'a value'; - -describe('AlertSummaryRow', () => { - let wrapper; - - function mountComponent({ mountMethod = shallowMount, props, defaultSlot } = {}) { - wrapper = mountMethod(AlertSummaryRow, { - propsData: props, - scopedSlots: { - default: defaultSlot, - }, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - describe('Alert Summary Row', () => { - beforeEach(() => { - mountComponent({ - props: { - label, - }, - defaultSlot: `${value}`, - }); - }); - - it('should display a label and a value', () => { - expect(wrapper.text()).toBe(`${label} ${value}`); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js deleted file mode 100644 index 00c479071fe..00000000000 --- a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js +++ /dev/null @@ -1,173 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { GlDropdownItem } from '@gitlab/ui'; -import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue'; -import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; -import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql'; -import mockAlerts from '../../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar Assignees', () => { - let wrapper; - let mock; - - function mountComponent({ - data, - users = [], - isDropdownSearching = false, - sidebarCollapsed = true, - loading = false, - stubs = {}, - } = {}) { - wrapper = shallowMount(SidebarAssignees, { - data() { - return { - users, - isDropdownSearching, - }; - }, - propsData: { - alert: { ...mockAlert }, - ...data, - sidebarCollapsed, - projectPath: 'projectPath', - projectId: '1', - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - mock.restore(); - }); - - const findAssigned = () => wrapper.find('[data-testid="assigned-users"]'); - const findUnassigned = () => wrapper.find('[data-testid="unassigned-users"]'); - - describe('updating the alert status', () => { - const mockUpdatedMutationResult = { - data: { - alertSetAssignees: { - errors: [], - alert: { - assigneeUsernames: ['root'], - }, - }, - }, - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - const path = '/-/autocomplete/users.json'; - const users = [ - { - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: 1, - name: 'User 1', - username: 'root', - }, - { - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: 2, - name: 'User 2', - username: 'not-root', - }, - ]; - - mock.onGet(path).replyOnce(200, users); - mountComponent({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - users, - stubs: { - SidebarAssignee, - }, - }); - }); - - it('renders a unassigned option', async () => { - wrapper.setData({ isDropdownSearching: false }); - await wrapper.vm.$nextTick(); - expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned'); - }); - - it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - wrapper.setData({ isDropdownSearching: false }); - - await wrapper.vm.$nextTick(); - wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: AlertSetAssignees, - variables: { - iid: '1527542', - assigneeUsernames: ['root'], - projectPath: 'projectPath', - }, - }); - }); - - it('emits an error when request contains error messages', () => { - wrapper.setData({ isDropdownSearching: false }); - const errorMutationResult = { - data: { - alertSetAssignees: { - errors: ['There was a problem for sure.'], - alert: {}, - }, - }, - }; - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult); - return wrapper.vm - .$nextTick() - .then(() => { - const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); - SideBarAssigneeItem.vm.$emit('update-alert-assignees'); - }) - .then(() => { - expect(wrapper.emitted('alert-error')).toBeDefined(); - }); - }); - - it('stops updating and cancels loading when the request fails', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - wrapper.vm.updateAlertAssignees('root'); - expect(findUnassigned().text()).toBe('assign yourself'); - }); - - it('shows a user avatar, username and full name when a user is set', () => { - mountComponent({ - data: { alert: mockAlerts[1] }, - sidebarCollapsed: false, - loading: false, - stubs: { - SidebarAssignee, - }, - }); - - expect(findAssigned().find('img').attributes('src')).toBe('/url'); - expect(findAssigned().find('.dropdown-menu-user-full-name').text()).toBe('root'); - expect(findAssigned().find('.dropdown-menu-user-username').text()).toBe('@root'); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js deleted file mode 100644 index 5235ae63fee..00000000000 --- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import AlertSidebar from '~/alert_management/components/alert_sidebar.vue'; -import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; -import mockAlerts from '../../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar', () => { - let wrapper; - let mock; - - function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) { - wrapper = mountMethod(AlertSidebar, { - data() { - return { - sidebarStatus: false, - }; - }, - propsData: { - alert, - }, - provide: { - projectPath: 'projectPath', - projectId: '1', - }, - stubs, - mocks: { - $apollo: { - queries: { - sidebarStatus: {}, - }, - }, - }, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - mock.restore(); - }); - - describe('the sidebar renders', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mountComponent(); - }); - - it('open as default', () => { - expect(wrapper.classes('right-sidebar-expanded')).toBe(true); - }); - - it('should render side bar assignee dropdown', () => { - mountComponent({ - mountMethod: mount, - alert: mockAlert, - }); - expect(wrapper.find(SidebarAssignees).exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js deleted file mode 100644 index 0b60a36cf54..00000000000 --- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js +++ /dev/null @@ -1,130 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; -import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; -import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue'; -import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql'; -import Tracking from '~/tracking'; -import mockAlerts from '../../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar Status', () => { - let wrapper; - const findStatusDropdown = () => wrapper.find(GlDropdown); - const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); - const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findStatusDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]'); - - function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { - wrapper = mount(AlertSidebarStatus, { - propsData: { - alert: { ...mockAlert }, - ...data, - sidebarCollapsed, - projectPath: 'projectPath', - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('Alert Sidebar Dropdown Status', () => { - beforeEach(() => { - mountComponent({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - }); - }); - - it('displays status dropdown', () => { - expect(findStatusDropdown().exists()).toBe(true); - }); - - it('displays the dropdown status header', () => { - expect(findStatusDropdownHeader().exists()).toBe(true); - }); - - describe('updating the alert status', () => { - const mockUpdatedMutationResult = { - data: { - updateAlertStatus: { - errors: [], - alert: { - status: 'acknowledged', - }, - }, - }, - }; - - beforeEach(() => { - mountComponent({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - }); - }); - - it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - findStatusDropdownItem().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateAlertStatusMutation, - variables: { - iid: '1527542', - status: 'TRIGGERED', - projectPath: 'projectPath', - }, - }); - }); - - it('stops updating when the request fails', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - findStatusDropdownItem().vm.$emit('click'); - expect(findStatusLoadingIcon().exists()).toBe(false); - expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alert: mockAlert }, - loading: false, - }); - }); - - it('should track alert status updates', () => { - Tracking.event.mockClear(); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); - findStatusDropdownItem().vm.$emit('click'); - const status = findStatusDropdownItem().text(); - setImmediate(() => { - const { category, action, label } = trackAlertStatusUpdateOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action, { - label, - property: status, - }); - }); - }); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js deleted file mode 100644 index 65cfc600d76..00000000000 --- a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; -import SystemNote from '~/alert_management/components/system_notes/system_note.vue'; -import mockAlerts from '../../mocks/alerts.json'; - -const mockAlert = mockAlerts[1]; - -describe('Alert Details System Note', () => { - let wrapper; - - function mountComponent({ stubs = {} } = {}) { - wrapper = shallowMount(SystemNote, { - propsData: { - note: { ...mockAlert.notes.nodes[0] }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - describe('System notes', () => { - beforeEach(() => { - mountComponent({}); - }); - - it('renders the correct system note', () => { - const noteId = wrapper.find('.note-wrapper').attributes('id'); - const iconName = wrapper.find(GlIcon).attributes('name'); - - expect(noteId).toBe('note_1628'); - expect(iconName).toBe(mockAlert.notes.nodes[0].systemNoteIconName); - }); - }); -}); diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json deleted file mode 100644 index 5267a4fe50d..00000000000 --- a/spec/frontend/alert_management/mocks/alerts.json +++ /dev/null @@ -1,71 +0,0 @@ -[ - { - "iid": "1527542", - "title": "SyntaxError: Invalid or unexpected token", - "severity": "CRITICAL", - "eventCount": 7, - "createdAt": "2020-04-17T23:18:14.996Z", - "startedAt": "2020-04-17T23:18:14.996Z", - "endedAt": "2020-04-17T23:18:14.996Z", - "status": "TRIGGERED", - "assignees": { "nodes": [] }, - "notes": { "nodes": [] }, - "todos": { "nodes": [] } - }, - { - "iid": "1527543", - "title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert", - "severity": "MEDIUM", - "eventCount": 1, - "startedAt": "2020-04-17T23:18:14.996Z", - "endedAt": "2020-04-17T23:18:14.996Z", - "status": "ACKNOWLEDGED", - "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] }, - "issueIid": "1", - "notes": { - "nodes": [ - { - "id": "gid://gitlab/Note/1628", - "author": { - "id": "gid://gitlab/User/1", - "state": "active", - "__typename": "User", - "avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "name": "Administrator", - "username": "root", - "webUrl": "http://192.168.1.4:3000/root" - }, - "systemNoteIconName": "user" - } - ] - }, - "todos": { "nodes": [] } - }, - { - "iid": "1527544", - "title": "SyntaxError: Invalid or unexpected token", - "severity": "LOW", - "eventCount": 4, - "startedAt": "2020-04-17T23:18:14.996Z", - "endedAt": "2020-04-17T23:18:14.996Z", - "status": "RESOLVED", - "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] }, - "notes": { - "nodes": [ - { - "id": "gid://gitlab/Note/1629", - "author": { - "id": "gid://gitlab/User/2", - "state": "active", - "__typename": "User", - "avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", - "name": "Administrator", - "username": "root", - "webUrl": "http://192.168.1.4:3000/root" - } - } - ] - }, - "todos": { "nodes": [] } - } -] diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap deleted file mode 100644 index ef68a6a2c32..00000000000 --- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = ` -"
-
Add new integrations
-
-
- - - - -
-
- -
-
-
-
- - - -
-
-
-
Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the GitLab documentation to learn more about configuring your endpoint. - -
- Webhook URL - -
-
- - -
- -
-
-
-
- Authorization key - -
-
- - -
- -
-
- -
- - - -
-
-
-
Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional). - - - -
-
- - -
-
-
-
-
" -`; diff --git a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/alert_mapping_builder_spec.js deleted file mode 100644 index 5d48ff02e35..00000000000 --- a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue'; -import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json'; -import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; - -describe('AlertMappingBuilder', () => { - let wrapper; - - function mountComponent() { - wrapper = shallowMount(AlertMappingBuilder, { - propsData: { - payloadFields: parsedMapping.samplePayload.payloadAlerFields.nodes, - mapping: parsedMapping.storedMapping.nodes, - }, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - beforeEach(() => { - mountComponent(); - }); - - const findColumnInRow = (row, column) => - wrapper.findAll('.gl-display-table-row').at(row).findAll('.gl-display-table-cell ').at(column); - - it('renders column captions', () => { - expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle); - expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle); - expect(findColumnInRow(0, 3).text()).toContain(i18n.columns.fallbackKeyTitle); - - const fallbackColumnIcon = findColumnInRow(0, 3).find(GlIcon); - expect(fallbackColumnIcon.exists()).toBe(true); - expect(fallbackColumnIcon.attributes('name')).toBe('question'); - expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip); - }); - - it('renders disabled form input for each mapped field', () => { - gitlabFields.forEach((field, index) => { - const input = findColumnInRow(index + 1, 0).find(GlFormInput); - expect(input.attributes('value')).toBe(`${field.label} (${field.type.join(' or ')})`); - expect(input.attributes('disabled')).toBe(''); - }); - }); - - it('renders right arrow next to each input', () => { - gitlabFields.forEach((field, index) => { - const arrow = findColumnInRow(index + 1, 1).find('.right-arrow'); - expect(arrow.exists()).toBe(true); - }); - }); - - it('renders mapping dropdown for each field', () => { - gitlabFields.forEach(({ compatibleTypes }, index) => { - const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); - const searchBox = dropdown.find(GlSearchBoxByType); - const dropdownItems = dropdown.findAll(GlDropdownItem); - const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const numberOfMappingOptions = nodes.filter(({ type }) => - type.some((t) => compatibleTypes.includes(t)), - ); - - expect(dropdown.exists()).toBe(true); - expect(searchBox.exists()).toBe(true); - expect(dropdownItems).toHaveLength(numberOfMappingOptions.length); - }); - }); - - it('renders fallback dropdown only for the fields that have fallback', () => { - gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => { - const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown); - expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks)); - - if (numberOfFallbacks) { - const searchBox = dropdown.find(GlSearchBoxByType); - const dropdownItems = dropdown.findAll(GlDropdownItem); - const { nodes } = parsedMapping.samplePayload.payloadAlerFields; - const numberOfMappingOptions = nodes.filter(({ type }) => - type.some((t) => compatibleTypes.includes(t)), - ); - - expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks)); - expect(dropdownItems).toHaveLength(numberOfMappingOptions.length); - } - }); - }); -}); diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js deleted file mode 100644 index 5a3874d055b..00000000000 --- a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import { GlTable, GlIcon, GlButton } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; -import Tracking from '~/tracking'; -import AlertIntegrationsList, { - i18n, -} from '~/alerts_settings/components/alerts_integrations_list.vue'; -import { trackAlertIntegrationsViewsOptions } from '~/alerts_settings/constants'; - -const mockIntegrations = [ - { - id: '1', - active: true, - name: 'Integration 1', - type: 'HTTP endpoint', - }, - { - id: '2', - active: false, - name: 'Integration 2', - type: 'HTTP endpoint', - }, -]; - -describe('AlertIntegrationsList', () => { - let wrapper; - const { trigger: triggerIntersection } = useMockIntersectionObserver(); - - function mountComponent({ data = {}, props = {} } = {}) { - wrapper = mount(AlertIntegrationsList, { - data() { - return { ...data }; - }, - propsData: { - integrations: mockIntegrations, - ...props, - }, - stubs: { - GlIcon: true, - GlButton: true, - }, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - beforeEach(() => { - mountComponent(); - }); - - const findTableComponent = () => wrapper.find(GlTable); - const findTableComponentRows = () => wrapper.find(GlTable).findAll('table tbody tr'); - const finsStatusCell = () => wrapper.findAll('[data-testid="integration-activated-status"]'); - - it('renders a table', () => { - expect(findTableComponent().exists()).toBe(true); - }); - - it('renders an empty state when no integrations provided', () => { - mountComponent({ props: { integrations: [] } }); - expect(findTableComponent().text()).toContain(i18n.emptyState); - }); - - it('renders an an edit and delete button for each integration', () => { - expect(findTableComponent().findAll(GlButton).length).toBe(4); - }); - - it('renders an highlighted row when a current integration is selected to edit', () => { - mountComponent({ data: { currentIntegration: { id: '1' } } }); - expect(findTableComponentRows().at(0).classes()).toContain('gl-bg-blue-50'); - }); - - describe('integration status', () => { - it('enabled', () => { - const cell = finsStatusCell().at(0); - const activatedIcon = cell.find(GlIcon); - expect(cell.text()).toBe(i18n.status.enabled.name); - expect(activatedIcon.attributes('name')).toBe('check-circle-filled'); - expect(activatedIcon.attributes('title')).toBe(i18n.status.enabled.tooltip); - }); - - it('disabled', () => { - const cell = finsStatusCell().at(1); - const notActivatedIcon = cell.find(GlIcon); - expect(cell.text()).toBe(i18n.status.disabled.name); - expect(notActivatedIcon.attributes('name')).toBe('warning-solid'); - expect(notActivatedIcon.attributes('title')).toBe(i18n.status.disabled.tooltip); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - mountComponent(); - jest.spyOn(Tracking, 'event'); - }); - - it('should NOT track alert list page views when list is collapsed', () => { - triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: false } }); - - expect(Tracking.event).not.toHaveBeenCalled(); - }); - - it('should track alert list page views only once when list is expanded', () => { - triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: true } }); - triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: true } }); - triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: true } }); - - const { category, action } = trackAlertIntegrationsViewsOptions; - expect(Tracking.event).toHaveBeenCalledTimes(1); - expect(Tracking.event).toHaveBeenCalledWith(category, action); - }); - }); -}); diff --git a/spec/frontend/alerts_settings/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_spec.js deleted file mode 100644 index 21cdec6f94c..00000000000 --- a/spec/frontend/alerts_settings/alerts_settings_form_spec.js +++ /dev/null @@ -1,351 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { - GlForm, - GlFormSelect, - GlCollapse, - GlFormInput, - GlToggle, - GlFormTextarea, -} from '@gitlab/ui'; -import waitForPromises from 'helpers/wait_for_promises'; -import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; -import { defaultAlertSettingsConfig } from './util'; -import { typeSet } from '~/alerts_settings/constants'; - -describe('AlertsSettingsFormNew', () => { - let wrapper; - const mockToastShow = jest.fn(); - - const createComponent = ({ - data = {}, - props = {}, - multipleHttpIntegrationsCustomMapping = false, - } = {}) => { - wrapper = mount(AlertsSettingsForm, { - data() { - return { ...data }; - }, - propsData: { - loading: false, - canAddIntegration: true, - ...props, - }, - provide: { - glFeatures: { multipleHttpIntegrationsCustomMapping }, - ...defaultAlertSettingsConfig, - }, - mocks: { - $toast: { - show: mockToastShow, - }, - }, - }); - }; - - const findForm = () => wrapper.find(GlForm); - const findSelect = () => wrapper.find(GlFormSelect); - const findFormSteps = () => wrapper.find(GlCollapse); - const findFormFields = () => wrapper.findAll(GlFormInput); - const findFormToggle = () => wrapper.find(GlToggle); - const findTestPayloadSection = () => wrapper.find(`[id = "test-integration"]`); - const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`); - const findSubmitButton = () => wrapper.find(`[type = "submit"]`); - const findMultiSupportText = () => - wrapper.find(`[data-testid="multi-integrations-not-supported"]`); - const findJsonTestSubmit = () => wrapper.find(`[data-testid="integration-test-and-submit"]`); - const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`); - const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - describe('with default values', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders the initial template', () => { - expect(wrapper.html()).toMatchSnapshot(); - }); - - it('render the initial form with only an integration type dropdown', () => { - expect(findForm().exists()).toBe(true); - expect(findSelect().exists()).toBe(true); - expect(findMultiSupportText().exists()).toBe(false); - expect(findFormSteps().attributes('visible')).toBeUndefined(); - }); - - it('shows the rest of the form when the dropdown is used', async () => { - const options = findSelect().findAll('option'); - await options.at(1).setSelected(); - - await wrapper.vm.$nextTick(); - - expect(findFormFields().at(0).isVisible()).toBe(true); - }); - - it('disables the dropdown and shows help text when multi integrations are not supported', async () => { - createComponent({ props: { canAddIntegration: false } }); - expect(findSelect().attributes('disabled')).toBe('disabled'); - expect(findMultiSupportText().exists()).toBe(true); - }); - - it('disabled the name input when the selected value is prometheus', async () => { - createComponent(); - const options = findSelect().findAll('option'); - await options.at(2).setSelected(); - - expect(findFormFields().at(0).attributes('disabled')).toBe('disabled'); - }); - }); - - describe('submitting integration form', () => { - it('allows for create-new-integration with the correct form values for HTTP', async () => { - createComponent(); - - const options = findSelect().findAll('option'); - await options.at(1).setSelected(); - - await findFormFields().at(0).setValue('Test integration'); - await findFormToggle().trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(findSubmitButton().exists()).toBe(true); - expect(findSubmitButton().text()).toBe('Save integration'); - - findForm().trigger('submit'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('create-new-integration')).toBeTruthy(); - expect(wrapper.emitted('create-new-integration')[0]).toEqual([ - { type: typeSet.http, variables: { name: 'Test integration', active: true } }, - ]); - }); - - it('allows for create-new-integration with the correct form values for PROMETHEUS', async () => { - createComponent(); - - const options = findSelect().findAll('option'); - await options.at(2).setSelected(); - - await findFormFields().at(0).setValue('Test integration'); - await findFormFields().at(1).setValue('https://test.com'); - await findFormToggle().trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(findSubmitButton().exists()).toBe(true); - expect(findSubmitButton().text()).toBe('Save integration'); - - findForm().trigger('submit'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('create-new-integration')).toBeTruthy(); - expect(wrapper.emitted('create-new-integration')[0]).toEqual([ - { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } }, - ]); - }); - - it('allows for update-integration with the correct form values for HTTP', async () => { - createComponent({ - data: { - selectedIntegration: typeSet.http, - currentIntegration: { id: '1', name: 'Test integration pre' }, - }, - props: { - loading: false, - }, - }); - - await findFormFields().at(0).setValue('Test integration post'); - await findFormToggle().trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(findSubmitButton().exists()).toBe(true); - expect(findSubmitButton().text()).toBe('Save integration'); - - findForm().trigger('submit'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('update-integration')).toBeTruthy(); - expect(wrapper.emitted('update-integration')[0]).toEqual([ - { type: typeSet.http, variables: { name: 'Test integration post', active: true } }, - ]); - }); - - it('allows for update-integration with the correct form values for PROMETHEUS', async () => { - createComponent({ - data: { - selectedIntegration: typeSet.prometheus, - currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' }, - }, - props: { - loading: false, - }, - }); - - await findFormFields().at(0).setValue('Test integration'); - await findFormFields().at(1).setValue('https://test-post.com'); - await findFormToggle().trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(findSubmitButton().exists()).toBe(true); - expect(findSubmitButton().text()).toBe('Save integration'); - - findForm().trigger('submit'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('update-integration')).toBeTruthy(); - expect(wrapper.emitted('update-integration')[0]).toEqual([ - { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } }, - ]); - }); - }); - - describe('submitting the integration with a JSON test payload', () => { - beforeEach(() => { - createComponent({ - data: { - selectedIntegration: typeSet.http, - currentIntegration: { id: '1', name: 'Test' }, - active: true, - }, - props: { - loading: false, - }, - }); - }); - - it('should not allow a user to test invalid JSON', async () => { - jest.useFakeTimers(); - await findJsonTextArea().setValue('Invalid JSON'); - - jest.runAllTimers(); - await wrapper.vm.$nextTick(); - - expect(findJsonTestSubmit().exists()).toBe(true); - expect(findJsonTestSubmit().text()).toBe('Save and test payload'); - expect(findJsonTestSubmit().props('disabled')).toBe(true); - }); - - it('should allow for the form to be automatically saved if the test payload is successfully submitted', async () => { - jest.useFakeTimers(); - await findJsonTextArea().setValue('{ "value": "value" }'); - - jest.runAllTimers(); - await wrapper.vm.$nextTick(); - expect(findJsonTestSubmit().props('disabled')).toBe(false); - }); - }); - - describe('Test payload section for HTTP integration', () => { - beforeEach(() => { - createComponent({ - multipleHttpIntegrationsCustomMapping: true, - props: { - currentIntegration: { - type: typeSet.http, - }, - }, - }); - }); - - describe.each` - active | resetSamplePayloadConfirmed | disabled - ${true} | ${true} | ${undefined} - ${false} | ${true} | ${'disabled'} - ${true} | ${false} | ${'disabled'} - ${false} | ${false} | ${'disabled'} - `('', ({ active, resetSamplePayloadConfirmed, disabled }) => { - const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed'; - const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled'; - const activeState = active ? 'active' : 'not active'; - - it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => { - wrapper.setData({ - customMapping: { samplePayload: true }, - active, - resetSamplePayloadConfirmed, - }); - await wrapper.vm.$nextTick(); - expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled); - }); - }); - - describe('action buttons for sample payload', () => { - describe.each` - resetSamplePayloadConfirmed | samplePayload | caption - ${false} | ${true} | ${'Edit payload'} - ${true} | ${false} | ${'Submit payload'} - ${true} | ${true} | ${'Submit payload'} - ${false} | ${false} | ${'Submit payload'} - `('', ({ resetSamplePayloadConfirmed, samplePayload, caption }) => { - const samplePayloadMsg = samplePayload ? 'was provided' : 'was not provided'; - const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed'; - - it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => { - wrapper.setData({ - selectedIntegration: typeSet.http, - customMapping: { samplePayload }, - resetSamplePayloadConfirmed, - }); - await wrapper.vm.$nextTick(); - expect(findActionBtn().text()).toBe(caption); - }); - }); - }); - - describe('Parsing payload', () => { - it('displays a toast message on successful parse', async () => { - jest.useFakeTimers(); - wrapper.setData({ - selectedIntegration: typeSet.http, - customMapping: { samplePayload: false }, - }); - await wrapper.vm.$nextTick(); - - findActionBtn().vm.$emit('click'); - jest.advanceTimersByTime(1000); - - await waitForPromises(); - - expect(mockToastShow).toHaveBeenCalledWith( - 'Sample payload has been parsed. You can now map the fields.', - ); - }); - }); - }); - - describe('Mapping builder section', () => { - describe.each` - featureFlag | integrationOption | visible - ${true} | ${1} | ${true} - ${true} | ${2} | ${false} - ${false} | ${1} | ${false} - ${false} | ${2} | ${false} - `('', ({ featureFlag, integrationOption, visible }) => { - const visibleMsg = visible ? 'is rendered' : 'is not rendered'; - const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled'; - const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus; - - it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => { - createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag }); - const options = findSelect().findAll('option'); - options.at(integrationOption).setSelected(); - await wrapper.vm.$nextTick(); - expect(findMappingBuilderSection().exists()).toBe(visible); - }); - }); - }); -}); diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js deleted file mode 100644 index 4d0732ca76c..00000000000 --- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js +++ /dev/null @@ -1,379 +0,0 @@ -import VueApollo from 'vue-apollo'; -import { mount, createLocalVue } from '@vue/test-utils'; -import AxiosMockAdapter from 'axios-mock-adapter'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; -import { GlLoadingIcon } from '@gitlab/ui'; -import axios from '~/lib/utils/axios_utils'; -import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue'; -import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; -import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; -import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; -import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; -import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql'; -import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; -import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql'; -import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql'; -import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql'; -import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql'; -import { typeSet } from '~/alerts_settings/constants'; -import { - ADD_INTEGRATION_ERROR, - RESET_INTEGRATION_TOKEN_ERROR, - UPDATE_INTEGRATION_ERROR, - INTEGRATION_PAYLOAD_TEST_ERROR, - DELETE_INTEGRATION_ERROR, -} from '~/alerts_settings/utils/error_messages'; -import createFlash from '~/flash'; -import { defaultAlertSettingsConfig } from './util'; -import mockIntegrations from './mocks/integrations.json'; -import { - createHttpVariables, - updateHttpVariables, - createPrometheusVariables, - updatePrometheusVariables, - ID, - errorMsg, - getIntegrationsQueryResponse, - destroyIntegrationResponse, - integrationToDestroy, - destroyIntegrationResponseWithErrors, -} from './mocks/apollo_mock'; - -jest.mock('~/flash'); - -const localVue = createLocalVue(); - -describe('AlertsSettingsWrapper', () => { - let wrapper; - let fakeApollo; - let destroyIntegrationHandler; - useMockIntersectionObserver(); - - const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon); - const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr'); - - async function destroyHttpIntegration(localWrapper) { - await jest.runOnlyPendingTimers(); - await localWrapper.vm.$nextTick(); - - localWrapper - .find(IntegrationsList) - .vm.$emit('delete-integration', { id: integrationToDestroy.id }); - } - - async function awaitApolloDomMock() { - await wrapper.vm.$nextTick(); // kick off the DOM update - await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises) - await wrapper.vm.$nextTick(); // kick off the DOM update for flash - } - - const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => { - wrapper = mount(AlertsSettingsWrapper, { - data() { - return { ...data }; - }, - provide: { - ...defaultAlertSettingsConfig, - ...provide, - }, - mocks: { - $apollo: { - mutate: jest.fn(), - query: jest.fn(), - queries: { - integrations: { - loading, - }, - }, - }, - }, - }); - }; - - function createComponentWithApollo({ - destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse), - } = {}) { - localVue.use(VueApollo); - destroyIntegrationHandler = destroyHandler; - - const requestHandlers = [ - [getIntegrationsQuery, jest.fn().mockResolvedValue(getIntegrationsQueryResponse)], - [destroyHttpIntegrationMutation, destroyIntegrationHandler], - ]; - - fakeApollo = createMockApollo(requestHandlers); - - wrapper = mount(AlertsSettingsWrapper, { - localVue, - apolloProvider: fakeApollo, - provide: { - ...defaultAlertSettingsConfig, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('rendered via default permissions', () => { - it('renders the GraphQL alerts integrations list and new form', () => { - createComponent(); - expect(wrapper.find(IntegrationsList).exists()).toBe(true); - expect(wrapper.find(AlertsSettingsForm).exists()).toBe(true); - }); - - it('uses a loading state inside the IntegrationsList table', () => { - createComponent({ - data: { integrations: {} }, - loading: true, - }); - expect(wrapper.find(IntegrationsList).exists()).toBe(true); - expect(findLoader().exists()).toBe(true); - }); - - it('renders the IntegrationsList table using the API data', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - expect(findLoader().exists()).toBe(false); - expect(findIntegrations()).toHaveLength(mockIntegrations.length); - }); - - it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ - data: { createHttpIntegrationMutation: { integration: { id: '1' } } }, - }); - wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', { - type: typeSet.http, - variables: createHttpVariables, - }); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: createHttpIntegrationMutation, - update: expect.anything(), - variables: createHttpVariables, - }); - }); - - it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ - data: { updateHttpIntegrationMutation: { integration: { id: '1' } } }, - }); - wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', { - type: typeSet.http, - variables: updateHttpVariables, - }); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateHttpIntegrationMutation, - variables: updateHttpVariables, - }); - }); - - it('calls `$apollo.mutate` with `resetHttpTokenMutation`', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ - data: { resetHttpTokenMutation: { integration: { id: '1' } } }, - }); - wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { - type: typeSet.http, - variables: { id: ID }, - }); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: resetHttpTokenMutation, - variables: { - id: ID, - }, - }); - }); - - it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ - data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } }, - }); - wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', { - type: typeSet.prometheus, - variables: createPrometheusVariables, - }); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: createPrometheusIntegrationMutation, - update: expect.anything(), - variables: createPrometheusVariables, - }); - }); - - it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ - data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } }, - }); - wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', { - type: typeSet.prometheus, - variables: updatePrometheusVariables, - }); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updatePrometheusIntegrationMutation, - variables: updatePrometheusVariables, - }); - }); - - it('calls `$apollo.mutate` with `resetPrometheusTokenMutation`', () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ - data: { resetPrometheusTokenMutation: { integration: { id: '1' } } }, - }); - wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { - type: typeSet.prometheus, - variables: { id: ID }, - }); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: resetPrometheusTokenMutation, - variables: { - id: ID, - }, - }); - }); - - it('shows an error alert when integration creation fails ', async () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR); - wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {}); - - await waitForPromises(); - - expect(createFlash).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR }); - }); - - it('shows an error alert when integration token reset fails ', async () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR); - - wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {}); - - await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR }); - }); - - it('shows an error alert when integration update fails ', async () => { - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); - - wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {}); - - await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR }); - }); - - it('shows an error alert when integration test payload fails ', async () => { - const mock = new AxiosMockAdapter(axios); - mock.onPost(/(.*)/).replyOnce(403); - createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, - loading: false, - }); - - return wrapper.vm.validateAlertPayload({ endpoint: '', data: '', token: '' }).then(() => { - expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); - expect(createFlash).toHaveBeenCalledTimes(1); - mock.restore(); - }); - }); - }); - - describe('with mocked Apollo client', () => { - it('has a selection of integrations loaded via the getIntegrationsQuery', async () => { - createComponentWithApollo(); - - await jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - - expect(findIntegrations()).toHaveLength(4); - }); - - it('calls a mutation with correct parameters and destroys a integration', async () => { - createComponentWithApollo(); - - await destroyHttpIntegration(wrapper); - - expect(destroyIntegrationHandler).toHaveBeenCalled(); - - await wrapper.vm.$nextTick(); - - expect(findIntegrations()).toHaveLength(3); - }); - - it('displays flash if mutation had a recoverable error', async () => { - createComponentWithApollo({ - destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors), - }); - - await destroyHttpIntegration(wrapper); - await awaitApolloDomMock(); - - expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' }); - }); - - it('displays flash if mutation had a non-recoverable error', async () => { - createComponentWithApollo({ - destroyHandler: jest.fn().mockRejectedValue('Error'), - }); - - await destroyHttpIntegration(wrapper); - await awaitApolloDomMock(); - - expect(createFlash).toHaveBeenCalledWith({ - message: DELETE_INTEGRATION_ERROR, - }); - }); - }); -}); diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap new file mode 100644 index 00000000000..eb2b82a0211 --- /dev/null +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap @@ -0,0 +1,406 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsSettingsForm with default values renders the initial template 1`] = ` +
+
+ Add new integrations +
+ +
+ +
+ + + + + + +
+
+ + +