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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/commands/metrics_server/metrics_server_spec.rb6
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb122
-rw-r--r--spec/config/inject_enterprise_edition_module_spec.rb2
-rw-r--r--spec/config/mail_room_spec.rb3
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb1
-rw-r--r--spec/controllers/admin/instance_review_controller_spec.rb1
-rw-r--r--spec/controllers/admin/runner_projects_controller_spec.rb59
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb38
-rw-r--r--spec/controllers/admin/users_controller_spec.rb35
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb12
-rw-r--r--spec/controllers/concerns/check_rate_limit_spec.rb85
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb18
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb82
-rw-r--r--spec/controllers/groups/packages_controller_spec.rb27
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb17
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/token_info_controller_spec.rb24
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb2
-rw-r--r--spec/controllers/profiles_controller_spec.rb28
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb18
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb5
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb9
-rw-r--r--spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb24
-rw-r--r--spec/controllers/projects/packages/packages_controller_spec.rb28
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb20
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb136
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb19
-rw-r--r--spec/controllers/projects/security/configuration_controller_spec.rb25
-rw-r--r--spec/controllers/projects/service_hook_logs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/services_controller_spec.rb4
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb13
-rw-r--r--spec/controllers/registrations_controller_spec.rb20
-rw-r--r--spec/controllers/search_controller_spec.rb27
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb28
-rw-r--r--spec/db/schema_spec.rb3
-rw-r--r--spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb53
-rw-r--r--spec/experiments/new_project_sast_enabled_experiment_spec.rb7
-rw-r--r--spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb59
-rw-r--r--spec/factories/ci/builds.rb9
-rw-r--r--spec/factories/ci/job_artifacts.rb4
-rw-r--r--spec/factories/ci/pipeline_message.rb9
-rw-r--r--spec/factories/ci/pipelines.rb4
-rw-r--r--spec/factories/ci/secure_files.rb10
-rw-r--r--spec/factories/clusters/agent_tokens.rb4
-rw-r--r--spec/factories/clusters/applications/helm.rb1
-rw-r--r--spec/factories/dependency_proxy.rb8
-rw-r--r--spec/factories/group/crm_settings.rb7
-rw-r--r--spec/factories/groups.rb6
-rw-r--r--spec/factories/incident_management/issuable_escalation_statuses.rb2
-rw-r--r--spec/factories/integrations.rb2
-rw-r--r--spec/factories/labels.rb2
-rw-r--r--spec/factories/namespaces.rb8
-rw-r--r--spec/factories/packages/package_files.rb6
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/factories/usage_data.rb20
-rw-r--r--spec/factories/users.rb2
-rw-r--r--spec/factories/wikis.rb2
-rw-r--r--spec/factories/work_items/work_item_types.rb (renamed from spec/factories/work_item/work_item_types.rb)14
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb125
-rw-r--r--spec/features/admin/admin_labels_spec.rb24
-rw-r--r--spec/features/admin/admin_runners_spec.rb146
-rw-r--r--spec/features/admin/admin_settings_spec.rb3
-rw-r--r--spec/features/admin/admin_users_spec.rb29
-rw-r--r--spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb15
-rw-r--r--spec/features/admin/users/user_spec.rb20
-rw-r--r--spec/features/admin/users/users_spec.rb4
-rw-r--r--spec/features/boards/board_filters_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb6
-rw-r--r--spec/features/commits_spec.rb54
-rw-r--r--spec/features/dashboard/issues_spec.rb2
-rw-r--r--spec/features/dashboard/milestones_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb20
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb6
-rw-r--r--spec/features/graphiql_spec.rb4
-rw-r--r--spec/features/groups/dependency_proxy_for_containers_spec.rb17
-rw-r--r--spec/features/groups/issues_spec.rb4
-rw-r--r--spec/features/groups/labels/edit_spec.rb14
-rw-r--r--spec/features/groups/labels/sort_labels_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups/navbar_spec.rb7
-rw-r--r--spec/features/groups/packages_spec.rb3
-rw-r--r--spec/features/groups/settings/access_tokens_spec.rb53
-rw-r--r--spec/features/groups_spec.rb22
-rw-r--r--spec/features/help_dropdown_spec.rb67
-rw-r--r--spec/features/help_pages_spec.rb17
-rw-r--r--spec/features/issuables/sorting_list_spec.rb16
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb70
-rw-r--r--spec/features/issues/service_desk_spec.rb2
-rw-r--r--spec/features/issues/user_bulk_edits_issues_spec.rb20
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb3
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb2
-rw-r--r--spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb33
-rw-r--r--spec/features/issues/user_sees_breadcrumb_links_spec.rb2
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb4
-rw-r--r--spec/features/markdown/mermaid_spec.rb4
-rw-r--r--spec/features/markdown/sandboxed_mermaid_spec.rb32
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/password_reset_spec.rb4
-rw-r--r--spec/features/profile_spec.rb65
-rw-r--r--spec/features/profiles/chat_names_spec.rb2
-rw-r--r--spec/features/profiles/emails_spec.rb2
-rw-r--r--spec/features/profiles/keys_spec.rb4
-rw-r--r--spec/features/profiles/password_spec.rb8
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb11
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb22
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb4
-rw-r--r--spec/features/projects/branches/user_deletes_branch_spec.rb24
-rw-r--r--spec/features/projects/branches_spec.rb24
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb18
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_lfs_files_spec.rb6
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb1
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb8
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb1
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb3
-rw-r--r--spec/features/projects/integrations/user_activates_jira_spec.rb8
-rw-r--r--spec/features/projects/labels/sort_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/user_edits_labels_spec.rb14
-rw-r--r--spec/features/projects/new_project_spec.rb38
-rw-r--r--spec/features/projects/packages_spec.rb3
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb97
-rw-r--r--spec/features/projects/services/user_activates_issue_tracker_spec.rb6
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_slack_notifications_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb4
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb162
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb2
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb90
-rw-r--r--spec/features/projects/user_creates_project_spec.rb17
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb4
-rw-r--r--spec/features/projects/view_on_env_spec.rb21
-rw-r--r--spec/features/protected_branches_spec.rb41
-rw-r--r--spec/features/runners_spec.rb25
-rw-r--r--spec/features/user_sees_marketing_header_spec.rb69
-rw-r--r--spec/features/user_sorts_things_spec.rb6
-rw-r--r--spec/features/users/anonymous_sessions_spec.rb2
-rw-r--r--spec/features/users/login_spec.rb24
-rw-r--r--spec/finders/ci/runners_finder_spec.rb12
-rw-r--r--spec/finders/environments/environments_by_deployments_finder_spec.rb12
-rw-r--r--spec/finders/fork_targets_finder_spec.rb8
-rw-r--r--spec/finders/group_descendants_finder_spec.rb334
-rw-r--r--spec/finders/group_members_finder_spec.rb116
-rw-r--r--spec/finders/groups/user_groups_finder_spec.rb17
-rw-r--r--spec/finders/merge_requests_finder_spec.rb101
-rw-r--r--spec/finders/packages/conan/package_file_finder_spec.rb30
-rw-r--r--spec/finders/packages/go/package_finder_spec.rb2
-rw-r--r--spec/finders/packages/maven/package_finder_spec.rb2
-rw-r--r--spec/finders/packages/npm/package_finder_spec.rb2
-rw-r--r--spec/finders/packages/nuget/package_finder_spec.rb2
-rw-r--r--spec/finders/packages/package_file_finder_spec.rb28
-rw-r--r--spec/finders/user_group_notification_settings_finder_spec.rb238
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb36
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json24
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json12
-rw-r--r--spec/fixtures/ci_secure_files/upload-keystore.jksbin0 -> 2760 bytes
-rw-r--r--spec/fixtures/error_tracking/go_two_exception_event.json1
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report.json10
-rw-r--r--spec/frontend/__helpers__/matchers.js68
-rw-r--r--spec/frontend/__helpers__/matchers/index.js3
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_sprite_icon.js36
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js35
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js65
-rw-r--r--spec/frontend/__helpers__/matchers/to_match_interpolated_text.js30
-rw-r--r--spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js46
-rw-r--r--spec/frontend/__helpers__/matchers_spec.js48
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js2
-rw-r--r--spec/frontend/__helpers__/wait_using_real_timer.js7
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js10
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js4
-rw-r--r--spec/frontend/api/packages_api_spec.js11
-rw-r--r--spec/frontend/behaviors/copy_to_clipboard_spec.js187
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap14
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js8
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js (renamed from spec/frontend/line_highlighter_spec.js)2
-rw-r--r--spec/frontend/blob/viewer/index_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_spec.js4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js20
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js22
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js8
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js12
-rw-r--r--spec/frontend/boards/stores/actions_spec.js32
-rw-r--r--spec/frontend/branches/branches_delete_modal_spec.js40
-rw-r--r--spec/frontend/ci_lint/components/ci_lint_spec.js2
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js8
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/agent_options_spec.js211
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js26
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js2
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js12
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/frontmatter_spec.js5
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js6
-rw-r--r--spec/frontend/content_editor/extensions/code_spec.js8
-rw-r--r--spec/frontend/content_editor/extensions/frontmatter_spec.js25
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js41
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js2
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js15
-rw-r--r--spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js6
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js6
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js2
-rw-r--r--spec/frontend/crm/contact_form_spec.js4
-rw-r--r--spec/frontend/crm/form_spec.js278
-rw-r--r--spec/frontend/crm/mock_data.js8
-rw-r--r--spec/frontend/crm/new_organization_form_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js57
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap41
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js50
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js36
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js10
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js27
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js51
-rw-r--r--spec/frontend/design_management/components/image_spec.js2
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js2
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js2
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap27
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js2
-rw-r--r--spec/frontend/design_management/pages/index_spec.js14
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js19
-rw-r--r--spec/frontend/editor/source_editor_spec.js15
-rw-r--r--spec/frontend/emoji/components/category_spec.js2
-rw-r--r--spec/frontend/emoji/components/emoji_list_spec.js2
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js8
-rw-r--r--spec/frontend/environments/deployment_spec.js29
-rw-r--r--spec/frontend/environments/deployment_status_badge_spec.js42
-rw-r--r--spec/frontend/environments/environment_actions_spec.js35
-rw-r--r--spec/frontend/environments/environment_stop_spec.js72
-rw-r--r--spec/frontend/environments/graphql/mock_data.js136
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js34
-rw-r--r--spec/frontend/environments/new_environment_folder_spec.js34
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js341
-rw-r--r--spec/frontend/environments/new_environments_app_spec.js37
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js28
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js2
-rw-r--r--spec/frontend/fixtures/blob.rb1
-rw-r--r--spec/frontend/fixtures/runner.rb117
-rw-r--r--spec/frontend/fixtures/static/project_select_combo_button.html2
-rw-r--r--spec/frontend/flash_spec.js255
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js2
-rw-r--r--spec/frontend/google_cloud/components/deployments_service_table_spec.js40
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js4
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js259
-rw-r--r--spec/frontend/groups/components/group_item_spec.js1
-rw-r--r--spec/frontend/groups/components/item_stats_spec.js1
-rw-r--r--spec/frontend/groups/landing_spec.js (renamed from spec/frontend/landing_spec.js)2
-rw-r--r--spec/frontend/groups/transfer_edit_spec.js (renamed from spec/frontend/transfer_edit_spec.js)2
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap9
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js20
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js11
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js18
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js13
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js413
-rw-r--r--spec/frontend/integrations/edit/store/actions_spec.js37
-rw-r--r--spec/frontend/integrations/edit/store/mutations_spec.js24
-rw-r--r--spec/frontend/integrations/edit/store/state_spec.js2
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js16
-rw-r--r--spec/frontend/integrations/overrides/components/integration_tabs_spec.js64
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js12
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js2
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js2
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js (renamed from spec/frontend/create_merge_request_dropdown_spec.js)2
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js (renamed from spec/frontend/issues_list/components/issue_card_time_info_spec.js)2
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js (renamed from spec/frontend/issues_list/components/issues_list_app_spec.js)16
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js (renamed from spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js)2
-rw-r--r--spec/frontend/issues/list/components/new_issue_dropdown_spec.js (renamed from spec/frontend/issues_list/components/new_issue_dropdown_spec.js)4
-rw-r--r--spec/frontend/issues/list/mock_data.js (renamed from spec/frontend/issues_list/mock_data.js)0
-rw-r--r--spec/frontend/issues/list/utils_spec.js (renamed from spec/frontend/issues_list/utils_spec.js)6
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js14
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js18
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js19
-rw-r--r--spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js (renamed from spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js)11
-rw-r--r--spec/frontend/issues/show/issue_spec.js6
-rw-r--r--spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap14
-rw-r--r--spec/frontend/issues_list/components/issuable_spec.js508
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js653
-rw-r--r--spec/frontend/issues_list/issuable_list_test_data.js77
-rw-r--r--spec/frontend/issues_list/service_desk_helper_spec.js28
-rw-r--r--spec/frontend/jira_import/utils/jira_import_utils_spec.js2
-rw-r--r--spec/frontend/jobs/bridge/app_spec.js123
-rw-r--r--spec/frontend/jobs/bridge/components/empty_state_spec.js7
-rw-r--r--spec/frontend/jobs/bridge/components/sidebar_spec.js97
-rw-r--r--spec/frontend/jobs/bridge/mock_data.js101
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js2
-rw-r--r--spec/frontend/labels/delete_label_modal_spec.js33
-rw-r--r--spec/frontend/lib/utils/resize_observer_spec.js68
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js2
-rw-r--r--spec/frontend/mr_popover/mr_popover_spec.js4
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js4
-rw-r--r--spec/frontend/notes/components/note_form_spec.js2
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js76
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js54
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js100
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js19
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap24
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js30
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js28
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js18
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js22
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js11
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js (renamed from spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js)38
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap (renamed from spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap (renamed from spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap)38
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js (renamed from spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_path_spec.js (renamed from spec/frontend/packages_and_registries/shared/package_path_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_tags_spec.js (renamed from spec/frontend/packages_and_registries/shared/package_tags_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js (renamed from spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js145
-rw-r--r--spec/frontend/packages_and_registries/shared/components/publish_method_spec.js (renamed from spec/frontend/packages_and_registries/shared/publish_method_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js (renamed from spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js)2
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js4
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js6
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js42
-rw-r--r--spec/frontend/pages/shared/nav/sidebar_tracking_spec.js36
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js211
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js40
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js64
-rw-r--r--spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js89
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js1
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js131
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap29
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js125
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js2
-rw-r--r--spec/frontend/profile/add_ssh_key_validation_spec.js36
-rw-r--r--spec/frontend/project_select_combo_button_spec.js4
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js10
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js2
-rw-r--r--spec/frontend/projects/project_find_file_spec.js (renamed from spec/frontend/project_find_file_spec.js)2
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js47
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js28
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js88
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js8
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js2
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js6
-rw-r--r--spec/frontend/repository/components/table/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js8
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js4
-rw-r--r--spec/frontend/repository/mock_data.js23
-rw-r--r--spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js (renamed from spec/frontend/runner/runner_detail/runner_details_app_spec.js)31
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js116
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js66
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js40
-rw-r--r--spec/frontend/runner/components/runner_header_spec.js93
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_status_badge_spec.js20
-rw-r--r--spec/frontend/runner/components/runner_type_alert_spec.js61
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js12
-rw-r--r--spec/frontend/runner/components/search_tokens/tag_token_spec.js6
-rw-r--r--spec/frontend/runner/components/stat/runner_online_stat_spec.js34
-rw-r--r--spec/frontend/runner/components/stat/runner_stats_spec.js46
-rw-r--r--spec/frontend/runner/components/stat/runner_status_stat_spec.js67
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js50
-rw-r--r--spec/frontend/runner/mock_data.js4
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js18
-rw-r--r--spec/frontend/runner/runner_update_form_utils_spec.js (renamed from spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js)7
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js38
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js181
-rw-r--r--spec/frontend/security_configuration/mock_data.js25
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap8
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js11
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js8
-rw-r--r--spec/frontend/sidebar/participants_spec.js8
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js2
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js2
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js2
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js5
-rw-r--r--spec/frontend/version_check_image_spec.js42
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js209
-rw-r--r--spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js29
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mock_data.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js178
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js2
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js69
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/test_extensions.js10
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js2
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/chronic_duration_input_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js77
-rw-r--r--spec/frontend/vue_shared/components/line_numbers_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap11
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer_spec.js47
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js155
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js2
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js31
-rw-r--r--spec/graphql/mutations/ci/runner/delete_spec.rb14
-rw-r--r--spec/graphql/mutations/clusters/agent_tokens/revoke_spec.rb55
-rw-r--r--spec/graphql/mutations/customer_relations/contacts/create_spec.rb15
-rw-r--r--spec/graphql/mutations/customer_relations/contacts/update_spec.rb2
-rw-r--r--spec/graphql/mutations/customer_relations/organizations/create_spec.rb2
-rw-r--r--spec/graphql/mutations/customer_relations/organizations/update_spec.rb13
-rw-r--r--spec/graphql/mutations/issues/set_escalation_status_spec.rb66
-rw-r--r--spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb9
-rw-r--r--spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb20
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb22
-rw-r--r--spec/graphql/resolvers/users/groups_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/work_items/types_resolver_spec.rb22
-rw-r--r--spec/graphql/types/ci/config/config_type_spec.rb1
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb1
-rw-r--r--spec/graphql/types/ci/pipeline_message_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/runner_type_spec.rb4
-rw-r--r--spec/graphql/types/clusters/agent_token_status_enum_spec.rb8
-rw-r--r--spec/graphql/types/clusters/agent_token_type_spec.rb2
-rw-r--r--spec/graphql/types/commit_type_spec.rb2
-rw-r--r--spec/graphql/types/group_member_relation_enum_spec.rb2
-rw-r--r--spec/graphql/types/group_type_spec.rb2
-rw-r--r--spec/graphql/types/incident_management/escalation_status_enum_spec.rb25
-rw-r--r--spec/graphql/types/issue_type_spec.rb47
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb26
-rw-r--r--spec/graphql/types/mutation_type_spec.rb8
-rw-r--r--spec/graphql/types/packages/package_details_type_spec.rb5
-rw-r--r--spec/graphql/types/project_type_spec.rb3
-rw-r--r--spec/graphql/types/projects/service_type_spec.rb2
-rw-r--r--spec/graphql/types/repository/blob_type_spec.rb6
-rw-r--r--spec/helpers/admin/background_migrations_helper_spec.rb16
-rw-r--r--spec/helpers/application_helper_spec.rb40
-rw-r--r--spec/helpers/application_settings_helper_spec.rb26
-rw-r--r--spec/helpers/auth_helper_spec.rb6
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb2
-rw-r--r--spec/helpers/button_helper_spec.rb1
-rw-r--r--spec/helpers/ci/jobs_helper_spec.rb10
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb2
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb9
-rw-r--r--spec/helpers/environment_helper_spec.rb10
-rw-r--r--spec/helpers/environments_helper_spec.rb2
-rw-r--r--spec/helpers/groups/crm_settings_helper_spec.rb25
-rw-r--r--spec/helpers/hooks_helper_spec.rb10
-rw-r--r--spec/helpers/integrations_helper_spec.rb28
-rw-r--r--spec/helpers/issues_helper_spec.rb23
-rw-r--r--spec/helpers/learn_gitlab_helper_spec.rb13
-rw-r--r--spec/helpers/namespaces_helper_spec.rb38
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb23
-rw-r--r--spec/helpers/operations_helper_spec.rb2
-rw-r--r--spec/helpers/packages_helper_spec.rb41
-rw-r--r--spec/helpers/projects/cluster_agents_helper_spec.rb5
-rw-r--r--spec/helpers/projects/issues/service_desk_helper_spec.rb54
-rw-r--r--spec/helpers/search_helper_spec.rb3
-rw-r--r--spec/helpers/snippets_helper_spec.rb3
-rw-r--r--spec/helpers/ssh_keys_helper_spec.rb25
-rw-r--r--spec/helpers/tree_helper_spec.rb18
-rw-r--r--spec/helpers/version_check_helper_spec.rb47
-rw-r--r--spec/initializers/doorkeeper_spec.rb2
-rw-r--r--spec/initializers/session_store_spec.rb36
-rw-r--r--spec/lib/api/entities/ci/pipeline_spec.rb21
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb3
-rw-r--r--spec/lib/api/helpers/rate_limiter_spec.rb73
-rw-r--r--spec/lib/backup/artifacts_spec.rb2
-rw-r--r--spec/lib/backup/files_spec.rb4
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb32
-rw-r--r--spec/lib/backup/gitaly_rpc_backup_spec.rb10
-rw-r--r--spec/lib/backup/lfs_spec.rb27
-rw-r--r--spec/lib/backup/manager_spec.rb6
-rw-r--r--spec/lib/backup/object_backup_spec.rb36
-rw-r--r--spec/lib/backup/repositories_spec.rb12
-rw-r--r--spec/lib/backup/repository_backup_error_spec.rb42
-rw-r--r--spec/lib/backup/uploads_spec.rb3
-rw-r--r--spec/lib/banzai/filter/footnote_filter_spec.rb46
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb153
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb72
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb5
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb47
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb238
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb41
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb156
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb8
-rw-r--r--spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb6
-rw-r--r--spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb31
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb27
-rw-r--r--spec/lib/error_tracking/collector/payload_validator_spec.rb32
-rw-r--r--spec/lib/feature_spec.rb32
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb1371
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb30
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb30
-rw-r--r--spec/lib/gitlab/auth_spec.rb30
-rw-r--r--spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb45
-rw-r--r--spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb46
-rw-r--r--spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb27
-rw-r--r--spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/base_job_spec.rb16
-rw-r--r--spec/lib/gitlab/background_migration/cleanup_concurrent_schema_change_spec.rb28
-rw-r--r--spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb232
-rw-r--r--spec/lib/gitlab/background_migration/job_coordinator_spec.rb45
-rw-r--r--spec/lib/gitlab/background_migration/migrate_legacy_artifacts_spec.rb158
-rw-r--r--spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb468
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb121
-rw-r--r--spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/changes_access_spec.rb80
-rw-r--r--spec/lib/gitlab/ci/build/status/reason_spec.rb75
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/jwt_v2_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/pipeline/logger_spec.rb84
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/tags/bulk_insert_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/trace/remote_checksum_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb196
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb36
-rw-r--r--spec/lib/gitlab/color_schemes_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/configurable_spec.rb9
-rw-r--r--spec/lib/gitlab/config/entry/factory_spec.rb11
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb6
-rw-r--r--spec/lib/gitlab/data_builder/archive_trace_spec.rb19
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb1
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb27
-rw-r--r--spec/lib/gitlab/database/background_migration_job_spec.rb2
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb76
-rw-r--r--spec/lib/gitlab/database/bulk_update_spec.rb2
-rw-r--r--spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb71
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb112
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb626
-rw-r--r--spec/lib/gitlab/database/migrations/runner_spec.rb2
-rw-r--r--spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb81
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb3
-rw-r--r--spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb7
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb43
-rw-r--r--spec/lib/gitlab/database/reflection_spec.rb60
-rw-r--r--spec/lib/gitlab/database/reindexing/coordinator_spec.rb76
-rw-r--r--spec/lib/gitlab/email/failure_handler_spec.rb69
-rw-r--r--spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb9
-rw-r--r--spec/lib/gitlab/event_store/event_spec.rb64
-rw-r--r--spec/lib/gitlab/event_store/store_spec.rb262
-rw-r--r--spec/lib/gitlab/exceptions_app_spec.rb68
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb8
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb24
-rw-r--r--spec/lib/gitlab/http_spec.rb34
-rw-r--r--spec/lib/gitlab/import/set_async_jid_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/lib/gitlab/import_export/avatar_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/base/relation_factory_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb41
-rw-r--r--spec/lib/gitlab/import_export/project/relation_factory_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb41
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/import_export/uploads_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/integrations/sti_type_spec.rb12
-rw-r--r--spec/lib/gitlab/jwt_authenticatable_spec.rb163
-rw-r--r--spec/lib/gitlab/lets_encrypt/client_spec.rb2
-rw-r--r--spec/lib/gitlab/lfs/client_spec.rb87
-rw-r--r--spec/lib/gitlab/logger_spec.rb94
-rw-r--r--spec/lib/gitlab/mail_room/authenticator_spec.rb188
-rw-r--r--spec/lib/gitlab/mail_room/mail_room_spec.rb63
-rw-r--r--spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb322
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb74
-rw-r--r--spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb21
-rw-r--r--spec/lib/gitlab/metrics/exporter/health_checks_middleware_spec.rb52
-rw-r--r--spec/lib/gitlab/metrics/exporter/metrics_middleware_spec.rb39
-rw-r--r--spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb53
-rw-r--r--spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb42
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data_spec.rb35
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb73
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb37
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb676
-rw-r--r--spec/lib/gitlab/redis/sessions_spec.rb73
-rw-r--r--spec/lib/gitlab/regex_spec.rb2
-rw-r--r--spec/lib/gitlab/search/params_spec.rb8
-rw-r--r--spec/lib/gitlab/shard_health_cache_spec.rb6
-rw-r--r--spec/lib/gitlab/sherlock/collection_spec.rb84
-rw-r--r--spec/lib/gitlab/sherlock/file_sample_spec.rb56
-rw-r--r--spec/lib/gitlab/sherlock/line_profiler_spec.rb75
-rw-r--r--spec/lib/gitlab/sherlock/line_sample_spec.rb35
-rw-r--r--spec/lib/gitlab/sherlock/location_spec.rb42
-rw-r--r--spec/lib/gitlab/sherlock/middleware_spec.rb81
-rw-r--r--spec/lib/gitlab/sherlock/query_spec.rb115
-rw-r--r--spec/lib/gitlab/sherlock/transaction_spec.rb238
-rw-r--r--spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb10
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb40
-rw-r--r--spec/lib/gitlab/sourcegraph_spec.rb6
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb41
-rw-r--r--spec/lib/gitlab/themes_spec.rb2
-rw-r--r--spec/lib/gitlab/tracking/standard_context_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb40
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb3
-rw-r--r--spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb77
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb55
-rw-r--r--spec/lib/gitlab/web_hooks/recursion_detection_spec.rb221
-rw-r--r--spec/lib/gitlab_edition_spec.rb160
-rw-r--r--spec/lib/gitlab_spec.rb131
-rw-r--r--spec/lib/sidebars/groups/menus/settings_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/panel_spec.rb3
-rw-r--r--spec/lib/version_check_spec.rb6
-rw-r--r--spec/mailers/emails/profile_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb10
-rw-r--r--spec/metrics_server/metrics_server_spec.rb42
-rw-r--r--spec/migrations/20210112143418_remove_duplicate_services2_spec.rb52
-rw-r--r--spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb30
-rw-r--r--spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb28
-rw-r--r--spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb47
-rw-r--r--spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb46
-rw-r--r--spec/migrations/20210226141517_dedup_issue_metrics_spec.rb66
-rw-r--r--spec/migrations/20210918202855_reschedule_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb30
-rw-r--r--spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb44
-rw-r--r--spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb (renamed from spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences3_spec.rb)55
-rw-r--r--spec/migrations/20211210140629_encrypt_static_object_token_spec.rb50
-rw-r--r--spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb36
-rw-r--r--spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb64
-rw-r--r--spec/migrations/add_has_external_issue_tracker_trigger_spec.rb164
-rw-r--r--spec/migrations/add_has_external_wiki_trigger_spec.rb128
-rw-r--r--spec/migrations/add_new_post_eoa_plans_spec.rb32
-rw-r--r--spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb48
-rw-r--r--spec/migrations/cleanup_projects_with_bad_has_external_issue_tracker_data_spec.rb94
-rw-r--r--spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb89
-rw-r--r--spec/migrations/drop_alerts_service_data_spec.rb21
-rw-r--r--spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb30
-rw-r--r--spec/migrations/remove_alerts_service_records_again_spec.rb23
-rw-r--r--spec/migrations/remove_alerts_service_records_spec.rb30
-rw-r--r--spec/migrations/reschedule_artifact_expiry_backfill_spec.rb38
-rw-r--r--spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb2
-rw-r--r--spec/migrations/schedule_populate_finding_uuid_for_vulnerability_feedback_spec.rb37
-rw-r--r--spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb127
-rw-r--r--spec/migrations/update_application_settings_protected_paths_spec.rb46
-rw-r--r--spec/migrations/update_invalid_member_states_spec.rb30
-rw-r--r--spec/models/alert_management/alert_spec.rb33
-rw-r--r--spec/models/application_record_spec.rb10
-rw-r--r--spec/models/application_setting_spec.rb46
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb6
-rw-r--r--spec/models/ci/build_report_result_spec.rb5
-rw-r--r--spec/models/ci/build_spec.rb228
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb11
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb12
-rw-r--r--spec/models/ci/freeze_period_spec.rb5
-rw-r--r--spec/models/ci/group_variable_spec.rb6
-rw-r--r--spec/models/ci/job_artifact_spec.rb27
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb5
-rw-r--r--spec/models/ci/namespace_mirror_spec.rb107
-rw-r--r--spec/models/ci/pending_build_spec.rb10
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb7
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb5
-rw-r--r--spec/models/ci/pipeline_spec.rb46
-rw-r--r--spec/models/ci/project_mirror_spec.rb34
-rw-r--r--spec/models/ci/resource_group_spec.rb5
-rw-r--r--spec/models/ci/runner_namespace_spec.rb6
-rw-r--r--spec/models/ci/runner_spec.rb112
-rw-r--r--spec/models/ci/running_build_spec.rb5
-rw-r--r--spec/models/ci/secure_file_spec.rb55
-rw-r--r--spec/models/ci/unit_test_spec.rb5
-rw-r--r--spec/models/clusters/agent_spec.rb27
-rw-r--r--spec/models/clusters/agent_token_spec.rb97
-rw-r--r--spec/models/clusters/agents/activity_event_spec.rb23
-rw-r--r--spec/models/clusters/applications/runner_spec.rb13
-rw-r--r--spec/models/commit_status_spec.rb49
-rw-r--r--spec/models/concerns/issuable_spec.rb16
-rw-r--r--spec/models/concerns/participable_spec.rb25
-rw-r--r--spec/models/concerns/routable_spec.rb33
-rw-r--r--spec/models/concerns/triggerable_hooks_spec.rb2
-rw-r--r--spec/models/container_repository_spec.rb8
-rw-r--r--spec/models/customer_relations/contact_spec.rb45
-rw-r--r--spec/models/customer_relations/issue_contact_spec.rb21
-rw-r--r--spec/models/dependency_proxy/blob_spec.rb1
-rw-r--r--spec/models/dependency_proxy/manifest_spec.rb1
-rw-r--r--spec/models/email_spec.rb82
-rw-r--r--spec/models/experiment_spec.rb48
-rw-r--r--spec/models/group/crm_settings_spec.rb15
-rw-r--r--spec/models/group_group_link_spec.rb26
-rw-r--r--spec/models/group_spec.rb384
-rw-r--r--spec/models/hooks/project_hook_spec.rb9
-rw-r--r--spec/models/hooks/service_hook_spec.rb30
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/instance_configuration_spec.rb4
-rw-r--r--spec/models/integration_spec.rb294
-rw-r--r--spec/models/integrations/asana_spec.rb132
-rw-r--r--spec/models/integrations/datadog_spec.rb33
-rw-r--r--spec/models/integrations/jira_spec.rb12
-rw-r--r--spec/models/internal_id_spec.rb6
-rw-r--r--spec/models/issue_spec.rb48
-rw-r--r--spec/models/key_spec.rb22
-rw-r--r--spec/models/member_spec.rb1
-rw-r--r--spec/models/merge_request_spec.rb83
-rw-r--r--spec/models/namespace_setting_spec.rb53
-rw-r--r--spec/models/namespace_spec.rb28
-rw-r--r--spec/models/namespaces/project_namespace_spec.rb4
-rw-r--r--spec/models/onboarding_progress_spec.rb85
-rw-r--r--spec/models/packages/package_file_spec.rb70
-rw-r--r--spec/models/packages/package_spec.rb19
-rw-r--r--spec/models/pages_domain_spec.rb123
-rw-r--r--spec/models/preloaders/environments/deployment_preloader_spec.rb65
-rw-r--r--spec/models/project_pages_metadatum_spec.rb11
-rw-r--r--spec/models/project_spec.rb412
-rw-r--r--spec/models/protectable_dropdown_spec.rb74
-rw-r--r--spec/models/ref_matcher_spec.rb83
-rw-r--r--spec/models/repository_spec.rb11
-rw-r--r--spec/models/route_spec.rb1
-rw-r--r--spec/models/user_spec.rb329
-rw-r--r--spec/models/users_statistics_spec.rb2
-rw-r--r--spec/models/work_items/type_spec.rb (renamed from spec/models/work_item/type_spec.rb)38
-rw-r--r--spec/policies/blob_policy_spec.rb7
-rw-r--r--spec/policies/group_member_policy_spec.rb18
-rw-r--r--spec/policies/group_policy_spec.rb186
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/presenters/blob_presenter_spec.rb20
-rw-r--r--spec/presenters/label_presenter_spec.rb25
-rw-r--r--spec/presenters/packages/conan/package_presenter_spec.rb34
-rw-r--r--spec/presenters/packages/detail/package_presenter_spec.rb22
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb21
-rw-r--r--spec/presenters/packages/nuget/package_metadata_presenter_spec.rb14
-rw-r--r--spec/presenters/packages/nuget/search_results_presenter_spec.rb4
-rw-r--r--spec/presenters/packages/pypi/package_presenter_spec.rb16
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/presenters/projects/security/configuration_presenter_spec.rb2
-rw-r--r--spec/presenters/service_hook_presenter_spec.rb4
-rw-r--r--spec/presenters/web_hook_log_presenter_spec.rb4
-rw-r--r--spec/rake_helper.rb2
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb65
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb423
-rw-r--r--spec/requests/api/ci/runners_spec.rb12
-rw-r--r--spec/requests/api/ci/triggers_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb8
-rw-r--r--spec/requests/api/generic_packages_spec.rb21
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb18
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb8
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb108
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb6
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb11
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb71
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb103
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb82
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb63
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb238
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/work_item_types_spec.rb71
-rw-r--r--spec/requests/api/groups_spec.rb48
-rw-r--r--spec/requests/api/integrations_spec.rb6
-rw-r--r--spec/requests/api/internal/base_spec.rb75
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb6
-rw-r--r--spec/requests/api/internal/mail_room_spec.rb194
-rw-r--r--spec/requests/api/lint_spec.rb19
-rw-r--r--spec/requests/api/maven_packages_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb35
-rw-r--r--spec/requests/api/package_files_spec.rb50
-rw-r--r--spec/requests/api/projects_spec.rb49
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb187
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb28
-rw-r--r--spec/requests/api/search_spec.rb24
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb37
-rw-r--r--spec/requests/api/usage_data_non_sql_metrics_spec.rb1
-rw-r--r--spec/requests/api/usage_data_queries_spec.rb1
-rw-r--r--spec/requests/api/users_spec.rb67
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/requests/groups/crm/contacts_controller_spec.rb16
-rw-r--r--spec/requests/groups/crm/organizations_controller_spec.rb16
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb90
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb103
-rw-r--r--spec/requests/projects/merge_requests/context_commit_diffs_spec.rb1
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb16
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb2
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb (renamed from spec/controllers/projects/settings/access_tokens_controller_spec.rb)47
-rw-r--r--spec/requests/rack_attack_global_spec.rb14
-rw-r--r--spec/requests/recursive_webhook_detection_spec.rb182
-rw-r--r--spec/requests/sandbox_controller_spec.rb14
-rw-r--r--spec/requests/users_controller_spec.rb13
-rw-r--r--spec/routing/routing_spec.rb6
-rw-r--r--spec/rubocop/code_reuse_helpers_spec.rb73
-rw-r--r--spec/rubocop/cop/database/establish_connection_spec.rb29
-rw-r--r--spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb58
-rw-r--r--spec/rubocop/cop/migration/schedule_async_spec.rb32
-rw-r--r--spec/scripts/setup/find_jh_branch_spec.rb97
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb8
-rw-r--r--spec/serializers/analytics_issue_entity_spec.rb8
-rw-r--r--spec/serializers/environment_serializer_spec.rb36
-rw-r--r--spec/serializers/group_child_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/services/alert_management/alerts/update_service_spec.rb53
-rw-r--r--spec/services/audit_event_service_spec.rb11
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb53
-rw-r--r--spec/services/branches/delete_merged_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/archive_extraction_service_spec.rb6
-rw-r--r--spec/services/bulk_imports/file_decompression_service_spec.rb18
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb32
-rw-r--r--spec/services/bulk_imports/file_export_service_spec.rb16
-rw-r--r--spec/services/bulk_imports/lfs_objects_export_service_spec.rb70
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb6
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb2
-rw-r--r--spec/services/ci/after_requeue_job_service_spec.rb2
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb19
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service/cache_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/custom_config_content_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/dry_run_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/include_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/logger_spec.rb8
-rw-r--r--spec/services/ci/create_pipeline_service/merge_requests_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/needs_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parallel_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parameter_content_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/tags_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb126
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb24
-rw-r--r--spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb17
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb47
-rw-r--r--spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb157
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb4
-rw-r--r--spec/services/ci/pipelines/add_job_service_spec.rb2
-rw-r--r--spec/services/ci/play_build_service_spec.rb14
-rw-r--r--spec/services/ci/process_sync_events_service_spec.rb26
-rw-r--r--spec/services/ci/register_job_service_spec.rb28
-rw-r--r--spec/services/ci/register_runner_service_spec.rb226
-rw-r--r--spec/services/ci/retry_build_service_spec.rb26
-rw-r--r--spec/services/clusters/agent_tokens/track_usage_service_spec.rb84
-rw-r--r--spec/services/clusters/agents/create_activity_event_service_spec.rb44
-rw-r--r--spec/services/clusters/agents/delete_expired_events_service_spec.rb36
-rw-r--r--spec/services/clusters/integrations/create_service_spec.rb2
-rw-r--r--spec/services/customer_relations/contacts/create_service_spec.rb4
-rw-r--r--spec/services/customer_relations/contacts/update_service_spec.rb4
-rw-r--r--spec/services/customer_relations/organizations/create_service_spec.rb2
-rw-r--r--spec/services/customer_relations/organizations/update_service_spec.rb4
-rw-r--r--spec/services/dependency_proxy/download_blob_service_spec.rb59
-rw-r--r--spec/services/dependency_proxy/find_cached_manifest_service_spec.rb4
-rw-r--r--spec/services/dependency_proxy/find_or_create_blob_service_spec.rb71
-rw-r--r--spec/services/deployments/archive_in_project_service_spec.rb11
-rw-r--r--spec/services/deployments/create_for_build_service_spec.rb82
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb2
-rw-r--r--spec/services/error_tracking/collect_error_service_spec.rb41
-rw-r--r--spec/services/events/destroy_service_spec.rb16
-rw-r--r--spec/services/feature_flags/hook_service_spec.rb2
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb10
-rw-r--r--spec/services/google_cloud/create_service_accounts_service_spec.rb45
-rw-r--r--spec/services/google_cloud/service_accounts_service_spec.rb19
-rw-r--r--spec/services/groups/update_service_spec.rb64
-rw-r--r--spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb51
-rw-r--r--spec/services/import/validate_remote_git_endpoint_service_spec.rb11
-rw-r--r--spec/services/incident_management/incidents/create_service_spec.rb6
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb56
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb108
-rw-r--r--spec/services/integrations/test/project_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb6
-rw-r--r--spec/services/issues/create_service_spec.rb21
-rw-r--r--spec/services/issues/move_service_spec.rb10
-rw-r--r--spec/services/issues/set_crm_contacts_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb123
-rw-r--r--spec/services/labels/transfer_service_spec.rb12
-rw-r--r--spec/services/members/destroy_service_spec.rb2
-rw-r--r--spec/services/members/invite_service_spec.rb2
-rw-r--r--spec/services/merge_requests/base_service_spec.rb2
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb8
-rw-r--r--spec/services/packages/create_event_service_spec.rb18
-rw-r--r--spec/services/packages/maven/metadata/sync_service_spec.rb18
-rw-r--r--spec/services/packages/terraform_module/create_package_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb3
-rw-r--r--spec/services/projects/destroy_service_spec.rb7
-rw-r--r--spec/services/projects/fork_service_spec.rb4
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb72
-rw-r--r--spec/services/projects/repository_languages_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_configuration_service_spec.rb76
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb78
-rw-r--r--spec/services/projects/update_service_spec.rb48
-rw-r--r--spec/services/protected_branches/create_service_spec.rb2
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb2
-rw-r--r--spec/services/protected_branches/update_service_spec.rb2
-rw-r--r--spec/services/protected_tags/create_service_spec.rb2
-rw-r--r--spec/services/protected_tags/destroy_service_spec.rb2
-rw-r--r--spec/services/protected_tags/update_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb2
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb58
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb102
-rw-r--r--spec/services/service_ping/submit_service_ping_service_spec.rb2
-rw-r--r--spec/services/test_hooks/system_service_spec.rb2
-rw-r--r--spec/services/users/create_service_spec.rb14
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb2
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb8
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb50
-rw-r--r--spec/services/web_hook_service_spec.rb139
-rw-r--r--spec/services/work_items/build_service_spec.rb20
-rw-r--r--spec/services/work_items/create_service_spec.rb72
-rw-r--r--spec/simplecov_env.rb1
-rw-r--r--spec/spec_helper.rb21
-rw-r--r--spec/support/database/cross-database-modification-allowlist.yml32
-rw-r--r--spec/support/db_cleaner.rb2
-rw-r--r--spec/support/flaky_tests.rb2
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci.yml8
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/gitaly_setup.rb204
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb7
-rw-r--r--spec/support/helpers/stub_object_storage.rb6
-rw-r--r--spec/support/helpers/test_env.rb132
-rw-r--r--spec/support/helpers/usage_data_helpers.rb4
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/praefect.rb4
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb39
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb57
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb165
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb127
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutation_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb50
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb10
-rw-r--r--spec/support/shared_examples/metrics/sampler_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb231
-rw-r--r--spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb25
-rw-r--r--spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb (renamed from spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb)46
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/work_item_base_types_importer.rb4
-rw-r--r--spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb14
-rw-r--r--spec/support/system_exit_detected.rb15
-rw-r--r--spec/support_specs/database/multiple_databases_spec.rb12
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb108
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/password_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb1
-rw-r--r--spec/tooling/danger/datateam_spec.rb113
-rw-r--r--spec/tooling/danger/project_helper_spec.rb2
-rw-r--r--spec/tooling/docs/deprecation_handling_spec.rb40
-rw-r--r--spec/uploaders/ci/secure_file_uploader_spec.rb72
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb13
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb48
-rw-r--r--spec/views/help/index.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/header/_gitlab_version.html.haml_spec.rb16
-rw-r--r--spec/views/profiles/keys/_form.html.haml_spec.rb6
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb24
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb28
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb30
-rw-r--r--spec/views/shared/access_tokens/_table.html.haml_spec.rb24
-rw-r--r--spec/views/shared/nav/_sidebar.html.haml_spec.rb3
-rw-r--r--spec/views/shared/wikis/_sidebar.html.haml_spec.rb2
-rw-r--r--spec/workers/ci/build_finished_worker_spec.rb15
-rw-r--r--spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb32
-rw-r--r--spec/workers/clusters/agents/delete_expired_events_worker_spec.rb30
-rw-r--r--spec/workers/concerns/application_worker_spec.rb67
-rw-r--r--spec/workers/concerns/cluster_agent_queue_spec.rb19
-rw-r--r--spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb36
-rw-r--r--spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb4
-rw-r--r--spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb12
-rw-r--r--spec/workers/deployments/hooks_worker_spec.rb4
-rw-r--r--spec/workers/email_receiver_worker_spec.rb78
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb3
-rw-r--r--spec/workers/loose_foreign_keys/cleanup_worker_spec.rb28
-rw-r--r--spec/workers/merge_requests/update_head_pipeline_worker_spec.rb138
-rw-r--r--spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb30
-rw-r--r--spec/workers/packages/cleanup_package_file_worker_spec.rb64
-rw-r--r--spec/workers/packages/cleanup_package_registry_worker_spec.rb57
-rw-r--r--spec/workers/pages_update_configuration_worker_spec.rb53
-rw-r--r--spec/workers/pages_worker_spec.rb16
-rw-r--r--spec/workers/purge_dependency_proxy_cache_worker_spec.rb6
-rw-r--r--spec/workers/web_hook_worker_spec.rb9
1057 files changed, 25386 insertions, 13909 deletions
diff --git a/spec/commands/metrics_server/metrics_server_spec.rb b/spec/commands/metrics_server/metrics_server_spec.rb
index f3936e6b346..b755801bb65 100644
--- a/spec/commands/metrics_server/metrics_server_spec.rb
+++ b/spec/commands/metrics_server/metrics_server_spec.rb
@@ -23,6 +23,8 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
end
context 'with a running server' do
+ let(:metrics_dir) { Dir.mktmpdir }
+
before do
# We need to send a request to localhost
WebMock.allow_net_connect!
@@ -33,7 +35,8 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
env = {
'GITLAB_CONFIG' => config_file.path,
'METRICS_SERVER_TARGET' => 'sidekiq',
- 'WIPE_METRICS_DIR' => '1'
+ 'WIPE_METRICS_DIR' => '1',
+ 'prometheus_multiproc_dir' => metrics_dir
}
@pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
end
@@ -55,6 +58,7 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
# 'No such process' means the process died before
ensure
config_file.unlink
+ FileUtils.rm_rf(metrics_dir, secure: true)
end
it 'serves /metrics endpoint' do
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 148b8720740..d7488e8d965 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require_relative '../../../sidekiq_cluster/cli'
-RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
+RSpec.describe Gitlab::SidekiqCluster::CLI, stubbing_settings_source: true do # rubocop:disable RSpec/FilePath
let(:cli) { described_class.new('/dev/null') }
let(:timeout) { Gitlab::SidekiqCluster::DEFAULT_SOFT_TIMEOUT_SECONDS }
let(:default_options) do
@@ -16,19 +16,39 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
let(:sidekiq_exporter_port) { '3807' }
let(:sidekiq_health_checks_port) { '3807' }
- before do
- stub_env('RAILS_ENV', 'test')
- stub_config(
- monitoring: {
- sidekiq_exporter: {
- enabled: sidekiq_exporter_enabled,
- port: sidekiq_exporter_port
- },
- sidekiq_health_checks: {
- port: sidekiq_health_checks_port
+ let(:config_file) { Tempfile.new('gitlab.yml') }
+ let(:config) do
+ {
+ 'test' => {
+ 'monitoring' => {
+ 'sidekiq_exporter' => {
+ 'address' => 'localhost',
+ 'enabled' => sidekiq_exporter_enabled,
+ 'port' => sidekiq_exporter_port
+ },
+ 'sidekiq_health_checks' => {
+ 'address' => 'localhost',
+ 'enabled' => sidekiq_exporter_enabled,
+ 'port' => sidekiq_health_checks_port
+ }
}
}
- )
+ }
+ end
+
+ before do
+ stub_env('RAILS_ENV', 'test')
+
+ config_file.write(YAML.dump(config))
+ config_file.close
+
+ allow(::Settings).to receive(:source).and_return(config_file.path)
+
+ ::Settings.reload!
+ end
+
+ after do
+ config_file.unlink
end
describe '#run' do
@@ -272,16 +292,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
context 'starting the server' do
context 'without --dryrun' do
context 'when there are no sidekiq_health_checks settings set' do
- before do
- stub_config(
- monitoring: {
- sidekiq_exporter: {
- enabled: true,
- port: sidekiq_exporter_port
- }
- }
- )
+ let(:sidekiq_exporter_enabled) { true }
+ before do
allow(Gitlab::SidekiqCluster).to receive(:start)
allow(cli).to receive(:write_pid)
allow(cli).to receive(:trap_signals)
@@ -293,25 +306,42 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
cli.run(%w(foo))
end
-
- it 'rescues Settingslogic::MissingSetting' do
- expect { cli.run(%w(foo)) }.not_to raise_error(Settingslogic::MissingSetting)
- end
end
context 'when the sidekiq_exporter.port setting is not set' do
+ let(:sidekiq_exporter_enabled) { true }
+
before do
- stub_config(
- monitoring: {
- sidekiq_exporter: {
- enabled: true
- },
- sidekiq_health_checks: {
- port: sidekiq_health_checks_port
+ allow(Gitlab::SidekiqCluster).to receive(:start)
+ allow(cli).to receive(:write_pid)
+ allow(cli).to receive(:trap_signals)
+ allow(cli).to receive(:start_loop)
+ end
+
+ it 'does not start a sidekiq metrics server' do
+ expect(MetricsServer).not_to receive(:spawn)
+
+ cli.run(%w(foo))
+ end
+ end
+
+ context 'when sidekiq_exporter.enabled setting is not set' do
+ let(:config) do
+ {
+ 'test' => {
+ 'monitoring' => {
+ 'sidekiq_exporter' => {},
+ 'sidekiq_health_checks' => {
+ 'address' => 'localhost',
+ 'enabled' => sidekiq_exporter_enabled,
+ 'port' => sidekiq_health_checks_port
+ }
}
}
- )
+ }
+ end
+ before do
allow(Gitlab::SidekiqCluster).to receive(:start)
allow(cli).to receive(:write_pid)
allow(cli).to receive(:trap_signals)
@@ -323,23 +353,21 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
cli.run(%w(foo))
end
-
- it 'rescues Settingslogic::MissingSetting' do
- expect { cli.run(%w(foo)) }.not_to raise_error(Settingslogic::MissingSetting)
- end
end
- context 'when sidekiq_exporter.enabled setting is not set' do
- before do
- stub_config(
- monitoring: {
- sidekiq_exporter: {},
- sidekiq_health_checks: {
- port: sidekiq_health_checks_port
+ context 'with a blank sidekiq_exporter setting' do
+ let(:config) do
+ {
+ 'test' => {
+ 'monitoring' => {
+ 'sidekiq_exporter' => nil,
+ 'sidekiq_health_checks' => nil
}
}
- )
+ }
+ end
+ before do
allow(Gitlab::SidekiqCluster).to receive(:start)
allow(cli).to receive(:write_pid)
allow(cli).to receive(:trap_signals)
@@ -351,6 +379,10 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
cli.run(%w(foo))
end
+
+ it 'does not throw an error' do
+ expect { cli.run(%w(foo)) }.not_to raise_error
+ end
end
context 'with valid settings' do
diff --git a/spec/config/inject_enterprise_edition_module_spec.rb b/spec/config/inject_enterprise_edition_module_spec.rb
index 61b40e46001..6ef74a2b616 100644
--- a/spec/config/inject_enterprise_edition_module_spec.rb
+++ b/spec/config/inject_enterprise_edition_module_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe InjectEnterpriseEditionModule do
before do
# Make sure we're not relying on which mode we're running under
- allow(Gitlab).to receive(:extensions).and_return([extension_name.downcase])
+ allow(GitlabEdition).to receive(:extensions).and_return([extension_name.downcase])
# Test on an imagined extension and imagined class
stub_const(fish_name, fish_class) # Fish
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 55f8fdd78ba..ec306837361 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -18,8 +18,9 @@ RSpec.describe 'mail_room.yml' do
result = Gitlab::Popen.popen_with_detail(%W(ruby -rerb -e #{cmd}), absolute_path('config'), vars)
output = result.stdout
+ errors = result.stderr
status = result.status
- raise "Error interpreting #{mailroom_config_path}: #{output}" unless status == 0
+ raise "Error interpreting #{mailroom_config_path}: #{output}\n#{errors}" unless status == 0
YAML.safe_load(output, permitted_classes: [Symbol])
end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 478bd1b7f0a..fb4c0970653 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -62,6 +62,7 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
describe 'GET #usage_data' do
before do
stub_usage_data_connections
+ stub_database_flavor_check
sign_in(admin)
end
diff --git a/spec/controllers/admin/instance_review_controller_spec.rb b/spec/controllers/admin/instance_review_controller_spec.rb
index 898cd30cdca..2169be4e70c 100644
--- a/spec/controllers/admin/instance_review_controller_spec.rb
+++ b/spec/controllers/admin/instance_review_controller_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Admin::InstanceReviewController do
before do
stub_application_setting(usage_ping_enabled: true)
stub_usage_data_connections
+ stub_database_flavor_check
::Gitlab::UsageData.data(force_refresh: true)
subject
end
diff --git a/spec/controllers/admin/runner_projects_controller_spec.rb b/spec/controllers/admin/runner_projects_controller_spec.rb
new file mode 100644
index 00000000000..e5f63025cf7
--- /dev/null
+++ b/spec/controllers/admin/runner_projects_controller_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::RunnerProjectsController do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ before do
+ sign_in(create(:admin))
+ end
+
+ describe '#create' do
+ let(:project_id) { project.path }
+
+ subject do
+ post :create, params: {
+ namespace_id: group.path,
+ project_id: project_id,
+ runner_project: { runner_id: project_runner.id }
+ }
+ end
+
+ context 'assigning runner to same project' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it 'redirects to the admin runner edit page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to edit_admin_runner_url(project_runner)
+ end
+ end
+
+ context 'assigning runner to another project' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [source_project]) }
+ let(:source_project) { create(:project) }
+
+ it 'redirects to the admin runner edit page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to edit_admin_runner_url(project_runner)
+ end
+ end
+
+ context 'for unknown project' do
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ let(:project_id) { 0 }
+
+ it 'shows 404 for unknown project' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index b9a59e9ae5f..08fb12c375e 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -26,6 +26,32 @@ RSpec.describe Admin::RunnersController do
render_views
let_it_be(:project) { create(:project) }
+
+ before_all do
+ create(:ci_build, runner: runner, project: project)
+ end
+
+ it 'shows a runner show page' do
+ get :show, params: { id: runner.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+
+ it 'when runner_read_only_admin_view is off, redirects to the runner edit page' do
+ stub_feature_flags(runner_read_only_admin_view: false)
+
+ get :show, params: { id: runner.id }
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to edit_admin_runner_path(runner)
+ end
+ end
+
+ describe '#edit' do
+ render_views
+
+ let_it_be(:project) { create(:project) }
let_it_be(:project_two) { create(:project) }
before_all do
@@ -33,29 +59,29 @@ RSpec.describe Admin::RunnersController do
create(:ci_build, runner: runner, project: project_two)
end
- it 'shows a particular runner' do
- get :show, params: { id: runner.id }
+ it 'shows a runner edit page' do
+ get :edit, params: { id: runner.id }
expect(response).to have_gitlab_http_status(:ok)
end
it 'shows 404 for unknown runner' do
- get :show, params: { id: 0 }
+ get :edit, params: { id: 0 }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'avoids N+1 queries', :request_store do
- get :show, params: { id: runner.id }
+ get :edit, params: { id: runner.id }
- control_count = ActiveRecord::QueryRecorder.new { get :show, params: { id: runner.id } }.count
+ control_count = ActiveRecord::QueryRecorder.new { get :edit, params: { id: runner.id } }.count
new_project = create(:project)
create(:ci_build, runner: runner, project: new_project)
# There is one additional query looking up subject.group in ProjectPolicy for the
# needs_new_sso_session permission
- expect { get :show, params: { id: runner.id } }.not_to exceed_query_limit(control_count + 1)
+ expect { get :edit, params: { id: runner.id } }.not_to exceed_query_limit(control_count + 1)
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 3a2b5dcb99d..c52223d4758 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -421,16 +421,37 @@ RSpec.describe Admin::UsersController do
end
describe 'PUT confirm/:id' do
- let(:user) { create(:user, confirmed_at: nil) }
+ shared_examples_for 'confirms the user' do
+ it 'confirms the user' do
+ put :confirm, params: { id: user.username }
+ user.reload
+ expect(user.confirmed?).to be_truthy
+ end
+ end
+
+ let(:expired_confirmation_sent_at) { Date.today - User.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at) { Date.today }
+
+ let(:user) do
+ create(:user, :unconfirmed).tap do |user|
+ user.update!(confirmation_sent_at: confirmation_sent_at)
+ end
+ end
before do
request.env["HTTP_REFERER"] = "/"
end
- it 'confirms user' do
- put :confirm, params: { id: user.username }
- user.reload
- expect(user.confirmed?).to be_truthy
+ context 'when the confirmation period has expired' do
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
+
+ it_behaves_like 'confirms the user'
+ end
+
+ context 'when the confirmation period has not expired' do
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
+
+ it_behaves_like 'confirms the user'
end
end
@@ -591,8 +612,8 @@ RSpec.describe Admin::UsersController do
end
context 'when the new password does not match the password confirmation' do
- let(:password) { 'some_password' }
- let(:password_confirmation) { 'not_same_as_password' }
+ let(:password) { Gitlab::Password.test_default }
+ let(:password_confirmation) { "not" + Gitlab::Password.test_default }
it 'shows the edit page again' do
update_password(user, password, password_confirmation)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index c2eb9d54303..6ccba866ebb 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -234,6 +234,18 @@ RSpec.describe AutocompleteController do
expect(json_response.first).to have_key('can_merge')
end
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get(:users, params: { search: 'foo@bar.com' })
+ end
+
+ before do
+ sign_in(current_user)
+ end
+ end
end
context 'GET projects' do
diff --git a/spec/controllers/concerns/check_rate_limit_spec.rb b/spec/controllers/concerns/check_rate_limit_spec.rb
new file mode 100644
index 00000000000..34ececfe639
--- /dev/null
+++ b/spec/controllers/concerns/check_rate_limit_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CheckRateLimit do
+ let(:key) { :some_key }
+ let(:scope) { [:some, :scope] }
+ let(:request) { instance_double('Rack::Request') }
+ let(:user) { build_stubbed(:user) }
+
+ let(:controller_class) do
+ Class.new do
+ include CheckRateLimit
+
+ attr_reader :request, :current_user
+
+ def initialize(request, current_user)
+ @request = request
+ @current_user = current_user
+ end
+
+ def redirect_back_or_default(**args)
+ end
+
+ def render(**args)
+ end
+ end
+ end
+
+ subject { controller_class.new(request, user) }
+
+ before do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?)
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:log_request)
+ end
+
+ describe '#check_rate_limit!' do
+ it 'calls ApplicationRateLimiter#throttled? with the right arguments' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false)
+ expect(subject).not_to receive(:render)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+
+ it 'renders error and logs request if throttled' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(true)
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user)
+ expect(subject).to receive(:render).with({ plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests })
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+
+ it 'redirects back if throttled and redirect_back option is set to true' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(true)
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user)
+ expect(subject).not_to receive(:render)
+ expect(subject).to receive(:redirect_back_or_default).with(options: { alert: _('This endpoint has been requested too many times. Try again later.') })
+
+ subject.check_rate_limit!(key, scope: scope, redirect_back: true)
+ end
+
+ context 'when the bypass header is set' do
+ before do
+ allow(Gitlab::Throttle).to receive(:bypass_header).and_return('SOME_HEADER')
+ end
+
+ it 'skips rate limit if set to "1"' do
+ allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('1')
+
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+ expect(subject).not_to receive(:render)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+
+ it 'does not skip rate limit if set to something else than "1"' do
+ allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('0')
+
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index ca4931bdc90..6201cddecb0 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -16,15 +16,6 @@ RSpec.describe Groups::BoardsController do
expect { list_boards }.to change(group.boards, :count).by(1)
end
- it 'pushes swimlanes_buffered_rendering feature flag' do
- allow(controller).to receive(:push_frontend_feature_flag).and_call_original
-
- expect(controller).to receive(:push_frontend_feature_flag)
- .with(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
-
- list_boards
- end
-
context 'when format is HTML' do
it 'renders template' do
list_boards
@@ -107,15 +98,6 @@ RSpec.describe Groups::BoardsController do
describe 'GET show' do
let!(:board) { create(:board, group: group) }
- it 'pushes swimlanes_buffered_rendering feature flag' do
- allow(controller).to receive(:push_frontend_feature_flag).and_call_original
-
- expect(controller).to receive(:push_frontend_feature_flag)
- .with(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
-
- read_board board: board
- end
-
context 'when format is HTML' do
it 'renders template' do
expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1)
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 0f262d93d4c..f438be534fa 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -178,10 +178,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
subject { get_manifest(tag) }
context 'feature enabled' do
- before do
- enable_dependency_proxy
- end
-
it_behaves_like 'without a token'
it_behaves_like 'without permission'
it_behaves_like 'feature flag disabled with private group'
@@ -270,7 +266,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
before do
- parent_group.create_dependency_proxy_setting!(enabled: true)
group_deploy_token.update_column(:group_id, parent_group.id)
end
@@ -294,10 +289,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
subject { get_blob }
context 'feature enabled' do
- before do
- enable_dependency_proxy
- end
-
it_behaves_like 'without a token'
it_behaves_like 'without permission'
it_behaves_like 'feature flag disabled with private group'
@@ -341,81 +332,12 @@ RSpec.describe Groups::DependencyProxyForContainersController do
let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
before do
- parent_group.create_dependency_proxy_setting!(enabled: true)
group_deploy_token.update_column(:group_id, parent_group.id)
end
it_behaves_like 'a successful blob pull'
end
end
-
- context 'when dependency_proxy_workhorse disabled' do
- let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
-
- before do
- stub_feature_flags(dependency_proxy_workhorse: false)
-
- allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
- allow(instance).to receive(:execute).and_return(blob_response)
- end
- end
-
- context 'remote blob request fails' do
- let(:blob_response) do
- {
- status: :error,
- http_status: 400,
- message: ''
- }
- end
-
- before do
- group.add_guest(user)
- end
-
- it 'proxies status from the remote blob request', :aggregate_failures do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to be_empty
- end
- end
-
- context 'a valid user' do
- before do
- group.add_guest(user)
- end
-
- it_behaves_like 'a successful blob pull'
- it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
-
- context 'with a cache entry' do
- let(:blob_response) { { status: :success, blob: blob, from_cache: true } }
-
- it_behaves_like 'returning response status', :success
- it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
- end
- end
-
- context 'a valid deploy token' do
- let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
- let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
-
- it_behaves_like 'a successful blob pull'
-
- context 'pulling from a subgroup' do
- let_it_be_with_reload(:parent_group) { create(:group) }
- let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
-
- before do
- parent_group.create_dependency_proxy_setting!(enabled: true)
- group_deploy_token.update_column(:group_id, parent_group.id)
- end
-
- it_behaves_like 'a successful blob pull'
- end
- end
- end
end
it_behaves_like 'not found when disabled'
@@ -542,10 +464,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
- def enable_dependency_proxy
- group.create_dependency_proxy_setting!(enabled: true)
- end
-
def disable_dependency_proxy
group.create_dependency_proxy_setting!(enabled: false)
end
diff --git a/spec/controllers/groups/packages_controller_spec.rb b/spec/controllers/groups/packages_controller_spec.rb
new file mode 100644
index 00000000000..fc9b79da47c
--- /dev/null
+++ b/spec/controllers/groups/packages_controller_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::PackagesController do
+ let_it_be(:group) { create(:group) }
+
+ let(:page) { :index }
+ let(:additional_parameters) { {} }
+
+ subject do
+ get page, params: additional_parameters.merge({
+ group_id: group
+ })
+ end
+
+ context 'GET #index' do
+ it_behaves_like 'returning response status', :ok
+ end
+
+ context 'GET #show' do
+ let(:page) { :show }
+ let(:additional_parameters) { { id: 1 } }
+
+ it_behaves_like 'returning response status', :ok
+ end
+end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 826625ba9c3..117c934ad5d 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -30,18 +30,27 @@ RSpec.describe Import::GitlabController do
expect(session[:gitlab_access_token]).to eq(token)
expect(controller).to redirect_to(status_import_gitlab_url)
end
+
+ it "importable_repos should return an array" do
+ allow_next_instance_of(Gitlab::GitlabImport::Client) do |instance|
+ allow(instance).to receive(:projects).and_return([{ "id": 1 }].to_enum)
+ end
+
+ expect(controller.send(:importable_repos)).to be_an_instance_of(Array)
+ end
end
describe "GET status" do
+ let(:repo_fake) { Struct.new(:id, :path, :path_with_namespace, :web_url, keyword_init: true) }
+ let(:repo) { repo_fake.new(id: 1, path: 'vim', path_with_namespace: 'asd/vim', web_url: 'https://gitlab.com/asd/vim') }
+
before do
- @repo = OpenStruct.new(id: 1, path: 'vim', path_with_namespace: 'asd/vim', web_url: 'https://gitlab.com/asd/vim')
assign_session_token
end
it_behaves_like 'import controller status' do
- let(:repo) { @repo }
- let(:repo_id) { @repo.id }
- let(:import_source) { @repo.path_with_namespace }
+ let(:repo_id) { repo.id }
+ let(:import_source) { repo.path_with_namespace }
let(:provider_name) { 'gitlab' }
let(:client_repos_field) { :projects }
end
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
index ecff173b8ac..29678706bba 100644
--- a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe Ldap::OmniauthCallbacksController do
end
context 'sign up' do
- let(:user) { double(email: +'new@example.com') }
+ let(:user) { create(:user) }
before do
stub_omniauth_setting(block_auto_created_users: false)
diff --git a/spec/controllers/oauth/token_info_controller_spec.rb b/spec/controllers/oauth/token_info_controller_spec.rb
index 6d01a534673..b66fff4d4e9 100644
--- a/spec/controllers/oauth/token_info_controller_spec.rb
+++ b/spec/controllers/oauth/token_info_controller_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe Oauth::TokenInfoController do
describe '#show' do
context 'when the user is not authenticated' do
- it 'responds with a 400' do
+ it 'responds with a 401' do
get :show
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_token')
end
end
@@ -36,11 +36,11 @@ RSpec.describe Oauth::TokenInfoController do
end
context 'when the doorkeeper_token is not recognised' do
- it 'responds with a 400' do
+ it 'responds with a 401' do
get :show, params: { access_token: 'unknown_token' }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_token')
end
end
@@ -49,22 +49,22 @@ RSpec.describe Oauth::TokenInfoController do
create(:oauth_access_token, created_at: 2.days.ago, expires_in: 10.minutes)
end
- it 'responds with a 400' do
+ it 'responds with a 401' do
get :show, params: { access_token: access_token.token }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_token')
end
end
context 'when the token is revoked' do
let(:access_token) { create(:oauth_access_token, revoked_at: 2.days.ago) }
- it 'responds with a 400' do
+ it 'responds with a 401' do
get :show, params: { access_token: access_token.token }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_token')
end
end
end
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
index 214a893f0fa..e41ae406d13 100644
--- a/spec/controllers/profiles/emails_controller_spec.rb
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Profiles::EmailsController do
end
context 'when email address is invalid' do
- let(:email) { 'invalid.@example.com' }
+ let(:email) { 'invalid@@example.com' }
it 'does not send an email confirmation' do
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 9a1f8a8442d..6e7cc058fbc 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -153,9 +153,12 @@ RSpec.describe ProfilesController, :request_store do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:new_username) { generate(:username) }
- it 'allows username change' do
+ before do
sign_in(user)
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
+ end
+ it 'allows username change' do
put :update_username,
params: { user: { username: new_username } }
@@ -166,8 +169,6 @@ RSpec.describe ProfilesController, :request_store do
end
it 'updates a username using JSON request' do
- sign_in(user)
-
put :update_username,
params: {
user: { username: new_username }
@@ -179,8 +180,6 @@ RSpec.describe ProfilesController, :request_store do
end
it 'renders an error message when the username was not updated' do
- sign_in(user)
-
put :update_username,
params: {
user: { username: 'invalid username.git' }
@@ -192,8 +191,6 @@ RSpec.describe ProfilesController, :request_store do
end
it 'raises a correct error when the username is missing' do
- sign_in(user)
-
expect { put :update_username, params: { user: { gandalf: 'you shall not pass' } } }
.to raise_error(ActionController::ParameterMissing)
end
@@ -202,8 +199,6 @@ RSpec.describe ProfilesController, :request_store do
it 'moves dependent projects to new namespace' do
project = create(:project_empty_repo, :legacy_storage, namespace: namespace)
- sign_in(user)
-
put :update_username,
params: { user: { username: new_username } }
@@ -220,8 +215,6 @@ RSpec.describe ProfilesController, :request_store do
before_disk_path = project.disk_path
- sign_in(user)
-
put :update_username,
params: { user: { username: new_username } }
@@ -232,5 +225,18 @@ RSpec.describe ProfilesController, :request_store do
expect(before_disk_path).to eq(project.disk_path)
end
end
+
+ context 'when the rate limit is reached' do
+ it 'does not update the username and returns status 429 Too Many Requests' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:profile_update_username, scope: user).and_return(true)
+
+ expect do
+ put :update_username,
+ params: { user: { username: new_username } }
+ end.not_to change { user.reload.username }
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 48a12a27911..cde3a8d4761 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -22,15 +22,6 @@ RSpec.describe Projects::BoardsController do
expect(assigns(:boards_endpoint)).to eq project_boards_path(project)
end
- it 'pushes swimlanes_buffered_rendering feature flag' do
- allow(controller).to receive(:push_frontend_feature_flag).and_call_original
-
- expect(controller).to receive(:push_frontend_feature_flag)
- .with(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
-
- list_boards
- end
-
context 'when format is HTML' do
it 'renders template' do
list_boards
@@ -125,15 +116,6 @@ RSpec.describe Projects::BoardsController do
describe 'GET show' do
let!(:board) { create(:board, project: project) }
- it 'pushes swimlanes_buffered_rendering feature flag' do
- allow(controller).to receive(:push_frontend_feature_flag).and_call_original
-
- expect(controller).to receive(:push_frontend_feature_flag)
- .with(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
-
- read_board board: board
- end
-
it 'sets boards_endpoint instance variable to a boards path' do
read_board board: board
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index edec8c3e9c6..596cd5c1a20 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -60,9 +60,9 @@ RSpec.describe Projects::MattermostsController do
it 'redirects to the new page' do
subject
- service = project.integrations.last
+ integration = project.integrations.last
- expect(subject).to redirect_to(edit_project_service_url(project, service))
+ expect(subject).to redirect_to(edit_project_integration_path(project, integration))
end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index f7370a1a1ac..a5c59b7e22d 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -205,7 +205,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
let(:expected_options) do
{
- environment: nil,
merge_request: merge_request,
merge_request_diff: merge_request.merge_request_diff,
merge_request_diffs: merge_request.merge_request_diffs,
@@ -280,7 +279,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
let(:expected_options) do
{
- environment: nil,
merge_request: merge_request,
merge_request_diff: merge_request.merge_request_diff,
merge_request_diffs: merge_request.merge_request_diffs,
@@ -303,7 +301,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:collection) { Gitlab::Diff::FileCollection::Commit }
let(:expected_options) do
{
- environment: nil,
merge_request: merge_request,
merge_request_diff: nil,
merge_request_diffs: merge_request.merge_request_diffs,
@@ -330,7 +327,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
let(:expected_options) do
{
- environment: nil,
merge_request: merge_request,
merge_request_diff: merge_request.merge_request_diff,
merge_request_diffs: merge_request.merge_request_diffs,
@@ -494,7 +490,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do
def collection_arguments(pagination_data = {})
{
- environment: nil,
merge_request: merge_request,
commit: nil,
diff_view: :inline,
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 66af546b113..2df31904380 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -762,9 +762,12 @@ RSpec.describe Projects::NotesController do
end
end
- it_behaves_like 'request exceeding rate limit', :clean_gitlab_redis_cache do
- let(:params) { request_params.except(:format) }
- let(:request_full_path) { project_notes_path(project) }
+ it_behaves_like 'create notes request exceeding rate limit', :clean_gitlab_redis_cache do
+ let(:current_user) { user }
+
+ def request
+ post :create, params: request_params.except(:format)
+ end
end
end
diff --git a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
index fc741d0f3f6..707edeaeee3 100644
--- a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
+++ b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
@@ -41,5 +41,29 @@ RSpec.describe Projects::Packages::InfrastructureRegistryController do
it_behaves_like 'returning response status', :not_found
end
+
+ context 'with package file pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: terraform_module) }
+
+ let(:terraform_module_package_file) { terraform_module.package_files.first }
+
+ it 'does not return them' do
+ subject
+
+ expect(assigns(:package_files)).to contain_exactly(terraform_module_package_file)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(assigns(:package_files)).to contain_exactly(package_file_pending_destruction, terraform_module_package_file)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/projects/packages/packages_controller_spec.rb b/spec/controllers/projects/packages/packages_controller_spec.rb
new file mode 100644
index 00000000000..da9cae47c62
--- /dev/null
+++ b/spec/controllers/projects/packages/packages_controller_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Packages::PackagesController do
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:page) { :index }
+ let(:additional_parameters) { {} }
+
+ subject do
+ get page, params: additional_parameters.merge({
+ project_id: project,
+ namespace_id: project.namespace
+ })
+ end
+
+ context 'GET #index' do
+ it_behaves_like 'returning response status', :ok
+ end
+
+ context 'GET #show' do
+ let(:page) { :show }
+ let(:additional_parameters) { { id: 1 } }
+
+ it_behaves_like 'returning response status', :ok
+ end
+end
diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
index 5338b77bd08..7dfa283195e 100644
--- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
@@ -141,7 +141,7 @@ RSpec.describe Projects::Prometheus::MetricsController do
expect(flash[:notice]).to include('Metric was successfully added.')
- expect(response).to redirect_to(edit_project_service_path(project, ::Integrations::Prometheus))
+ expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus))
end
end
@@ -157,6 +157,22 @@ RSpec.describe Projects::Prometheus::MetricsController do
end
end
+ describe 'PUT #update' do
+ context 'metric is updated' do
+ let_it_be(:metric) { create(:prometheus_metric, project: project) }
+
+ let(:metric_params) { { prometheus_metric: { title: 'new_title' }, id: metric.id } }
+
+ it 'shows a success flash message' do
+ put :update, params: project_params(metric_params)
+
+ expect(metric.reload.title).to eq('new_title')
+ expect(flash[:notice]).to include('Metric was successfully updated.')
+ expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus))
+ end
+ end
+ end
+
describe 'DELETE #destroy' do
context 'format html' do
let!(:metric) { create(:prometheus_metric, project: project) }
@@ -164,7 +180,7 @@ RSpec.describe Projects::Prometheus::MetricsController do
it 'destroys the metric' do
delete :destroy, params: project_params(id: metric.id)
- expect(response).to redirect_to(edit_project_service_path(project, ::Integrations::Prometheus))
+ expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus))
expect(PrometheusMetric.find_by(id: metric.id)).to be_nil
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 4d99afb6b1f..e0d88fa799f 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Projects::RawController do
let_it_be(:project) { create(:project, :public, :repository) }
let(:inline) { nil }
+ let(:params) { {} }
describe 'GET #show' do
def get_show
@@ -15,9 +16,9 @@ RSpec.describe Projects::RawController do
params: {
namespace_id: project.namespace,
project_id: project,
- id: filepath,
+ id: file_path,
inline: inline
- })
+ }.merge(params))
end
subject { get_show }
@@ -33,7 +34,7 @@ RSpec.describe Projects::RawController do
end
context 'regular filename' do
- let(:filepath) { 'master/CONTRIBUTING.md' }
+ let(:file_path) { 'master/CONTRIBUTING.md' }
it 'delivers ASCII file' do
allow(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original
@@ -60,7 +61,7 @@ RSpec.describe Projects::RawController do
end
context 'image header' do
- let(:filepath) { 'master/files/images/6049019_460s.jpg' }
+ let(:file_path) { 'master/files/images/6049019_460s.jpg' }
it 'leaves image content disposition' do
subject
@@ -77,44 +78,30 @@ RSpec.describe Projects::RawController do
context 'with LFS files' do
let(:filename) { 'lfs_object.iso' }
- let(:filepath) { "be93687/files/lfs/#{filename}" }
+ let(:file_path) { "be93687/files/lfs/#{filename}" }
it_behaves_like 'a controller that can serve LFS files'
it_behaves_like 'project cache control headers'
include_examples 'single Gitaly request'
end
- context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_rate_limiting do
+ context 'when the endpoint receives requests above the limit' do
let(:file_path) { 'master/README.md' }
+ let(:path_without_ref) { 'README.md' }
before do
- stub_application_setting(raw_blob_request_limit: 5)
+ allow(::Gitlab::ApplicationRateLimiter).to(
+ receive(:throttled?).with(:raw_blob, scope: [project, path_without_ref]).and_return(true)
+ )
end
- it 'prevents from accessing the raw file', :request_store do
- execute_raw_requests(requests: 5, project: project, file_path: file_path)
-
- expect { execute_raw_requests(requests: 1, project: project, file_path: file_path) }
- .to change { Gitlab::GitalyClient.get_request_count }.by(0)
+ it 'prevents from accessing the raw file' do
+ expect { get_show }.not_to change { Gitlab::GitalyClient.get_request_count }
expect(response.body).to eq(_('You cannot access the raw file. Please wait a minute.'))
expect(response).to have_gitlab_http_status(:too_many_requests)
end
- it 'logs the event on auth.log', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345889' do
- attributes = {
- message: 'Application_Rate_Limiter_Request',
- env: :raw_blob_request_limit,
- remote_ip: '0.0.0.0',
- request_method: 'GET',
- path: "/#{project.full_path}/-/raw/#{file_path}"
- }
-
- expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
-
- execute_raw_requests(requests: 6, project: project, file_path: file_path)
- end
-
context 'when receiving an external storage request' do
let(:token) { 'letmein' }
@@ -126,62 +113,10 @@ RSpec.describe Projects::RawController do
end
it 'does not prevent from accessing the raw file' do
- request.headers['X-Gitlab-External-Storage-Token'] = token
- execute_raw_requests(requests: 6, project: project, file_path: file_path)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when the request uses a different version of a commit' do
- it 'prevents from accessing the raw file' do
- # 3 times with the normal sha
- commit_sha = project.repository.commit.sha
- file_path = "#{commit_sha}/README.md"
-
- execute_raw_requests(requests: 3, project: project, file_path: file_path)
-
- # 3 times with the modified version
- modified_sha = commit_sha.gsub(commit_sha[0..5], commit_sha[0..5].upcase)
- modified_path = "#{modified_sha}/README.md"
-
- execute_raw_requests(requests: 3, project: project, file_path: modified_path)
-
- expect(response.body).to eq(_('You cannot access the raw file. Please wait a minute.'))
- expect(response).to have_gitlab_http_status(:too_many_requests)
- end
- end
-
- context 'when the throttling has been disabled' do
- before do
- stub_application_setting(raw_blob_request_limit: 0)
- end
-
- it 'does not prevent from accessing the raw file' do
- execute_raw_requests(requests: 10, project: project, file_path: file_path)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'with case-sensitive files' do
- it 'prevents from accessing the specific file' do
- create_file_in_repo(project, 'master', 'master', 'readme.md', 'Add readme.md')
- create_file_in_repo(project, 'master', 'master', 'README.md', 'Add README.md')
-
- commit_sha = project.repository.commit.sha
- file_path = "#{commit_sha}/readme.md"
-
- # Accessing downcase version of readme
- execute_raw_requests(requests: 6, project: project, file_path: file_path)
-
- expect(response.body).to eq(_('You cannot access the raw file. Please wait a minute.'))
- expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
- # Accessing upcase version of readme
- file_path = "#{commit_sha}/README.md"
-
- execute_raw_requests(requests: 1, project: project, file_path: file_path)
+ request.headers['X-Gitlab-External-Storage-Token'] = token
+ get_show
expect(response).to have_gitlab_http_status(:ok)
end
@@ -201,7 +136,7 @@ RSpec.describe Projects::RawController do
context 'when no token is provided' do
it 'redirects to sign in page' do
- execute_raw_requests(requests: 1, project: project, file_path: file_path)
+ get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
@@ -209,13 +144,11 @@ RSpec.describe Projects::RawController do
end
context 'when a token param is present' do
- subject(:execute_raw_request_with_token_in_params) do
- execute_raw_requests(requests: 1, project: project, file_path: file_path, token: token)
- end
-
context 'when token is correct' do
+ let(:params) { { token: token } }
+
it 'calls the action normally' do
- execute_raw_request_with_token_in_params
+ get_show
expect(response).to have_gitlab_http_status(:ok)
end
@@ -224,7 +157,7 @@ RSpec.describe Projects::RawController do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'redirects to sign in page' do
- execute_raw_request_with_token_in_params
+ get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
@@ -236,7 +169,7 @@ RSpec.describe Projects::RawController do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'calls the action normally' do
- execute_raw_request_with_token_in_params
+ get_show
expect(response).to have_gitlab_http_status(:ok)
end
@@ -245,10 +178,10 @@ RSpec.describe Projects::RawController do
end
context 'when token is incorrect' do
- let(:token) { 'foobar' }
+ let(:params) { { token: 'foobar' } }
it 'redirects to sign in page' do
- execute_raw_request_with_token_in_params
+ get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
@@ -257,14 +190,13 @@ RSpec.describe Projects::RawController do
end
context 'when a token header is present' do
- subject(:execute_raw_request_with_token_in_headers) do
+ before do
request.headers['X-Gitlab-Static-Object-Token'] = token
- execute_raw_requests(requests: 1, project: project, file_path: file_path)
end
context 'when token is correct' do
it 'calls the action normally' do
- execute_raw_request_with_token_in_headers
+ get_show
expect(response).to have_gitlab_http_status(:ok)
end
@@ -273,7 +205,7 @@ RSpec.describe Projects::RawController do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'redirects to sign in page' do
- execute_raw_request_with_token_in_headers
+ get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
@@ -285,7 +217,7 @@ RSpec.describe Projects::RawController do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'calls the action normally' do
- execute_raw_request_with_token_in_headers
+ get_show
expect(response).to have_gitlab_http_status(:ok)
end
@@ -297,7 +229,7 @@ RSpec.describe Projects::RawController do
let(:token) { 'foobar' }
it 'redirects to sign in page' do
- execute_raw_request_with_token_in_headers
+ get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
@@ -344,14 +276,4 @@ RSpec.describe Projects::RawController do
end
end
end
-
- def execute_raw_requests(requests:, project:, file_path:, **params)
- requests.times do
- get :show, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: file_path
- }.merge(params)
- end
- end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index f7cf55d8a95..1370ec9cc0b 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -210,6 +210,25 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:found)
end
end
+
+ context 'when token is migrated' do
+ let(:user) { create(:user, static_object_token: '') }
+ let(:token) { 'Test' }
+
+ it 'calls the action normally' do
+ user.update_column(:static_object_token, token)
+
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: token }, format: 'zip'
+ expect(user.static_object_token).to eq(token)
+ expect(response).to have_gitlab_http_status(:ok)
+
+ user.update_column(:static_object_token_encrypted, Gitlab::CryptoHelper.aes256_gcm_encrypt(token))
+
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: token }, format: 'zip'
+ expect(user.static_object_token).to eq(token)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
context 'when a token header is present' do
diff --git a/spec/controllers/projects/security/configuration_controller_spec.rb b/spec/controllers/projects/security/configuration_controller_spec.rb
index 848db16fb02..1ce0fcd85db 100644
--- a/spec/controllers/projects/security/configuration_controller_spec.rb
+++ b/spec/controllers/projects/security/configuration_controller_spec.rb
@@ -36,6 +36,31 @@ RSpec.describe Projects::Security::ConfigurationController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
+
+ it 'responds with configuration data json' do
+ get :show, params: { namespace_id: project.namespace, project_id: project, format: :json }
+
+ features = json_response['features']
+ sast_feature = features.find { |feature| feature['type'] == 'sast' }
+ dast_feature = features.find { |feature| feature['type'] == 'dast' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(sast_feature['available']).to be_truthy
+ expect(dast_feature['available']).to be_falsey
+ end
+
+ context 'with feature flag unify_security_configuration turned off' do
+ before do
+ stub_feature_flags(unify_security_configuration: false)
+ end
+
+ it 'responds with empty configuration data json' do
+ get :show, params: { namespace_id: project.namespace, project_id: project, format: :json }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/service_hook_logs_controller_spec.rb b/spec/controllers/projects/service_hook_logs_controller_spec.rb
index 9caa4a06b44..be78668aa88 100644
--- a/spec/controllers/projects/service_hook_logs_controller_spec.rb
+++ b/spec/controllers/projects/service_hook_logs_controller_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Projects::ServiceHookLogsController do
{
namespace_id: project.namespace,
project_id: project,
- service_id: integration.to_param,
+ integration_id: integration.to_param,
id: log.id
}
end
@@ -44,7 +44,7 @@ RSpec.describe Projects::ServiceHookLogsController do
it 'executes the hook and redirects to the service form' do
expect_any_instance_of(ServiceHook).to receive(:execute)
expect_any_instance_of(described_class).to receive(:set_hook_execution_notice)
- expect(subject).to redirect_to(edit_project_service_path(project, integration))
+ expect(subject).to redirect_to(edit_project_integration_path(project, integration))
end
it 'renders a 404 if the hook does not exist' do
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 29988da6e60..f3c7b501faa 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -183,7 +183,7 @@ RSpec.describe Projects::ServicesController do
let(:params) { project_params(service: integration_params) }
let(:message) { 'Jira settings saved and active.' }
- let(:redirect_url) { edit_project_service_path(project, integration) }
+ let(:redirect_url) { edit_project_integration_path(project, integration) }
before do
stub_jira_integration_test
@@ -341,7 +341,7 @@ RSpec.describe Projects::ServicesController do
it 'redirects user back to edit page with alert' do
put :update, params: project_params.merge(service: integration_params)
- expect(response).to redirect_to(edit_project_service_path(project, integration))
+ expect(response).to redirect_to(edit_project_integration_path(project, integration))
expected_alert = [
"You can now manage your Prometheus settings on the",
%(<a href="#{project_settings_operations_path(project)}">Operations</a> page.),
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index d50f1aa1dd8..7e96e99640a 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -25,6 +25,19 @@ RSpec.describe Projects::Settings::CiCdController do
expect(response).to render_template(:show)
end
+ context 'when the FF ci_owned_runners_cross_joins_fix is disabled' do
+ before do
+ stub_feature_flags(ci_owned_runners_cross_joins_fix: false)
+ end
+
+ it 'renders show with 200 status code' do
+ get :show, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+
context 'with CI/CD disabled' do
before do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 3f7941b3456..d5fe32ac094 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -20,6 +20,10 @@ RSpec.describe RegistrationsController do
end
describe '#create' do
+ before do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
+ end
+
let_it_be(:base_user_params) do
{ first_name: 'first', last_name: 'last', username: 'new_username', email: 'new@user.com', password: 'Any_password' }
end
@@ -410,6 +414,18 @@ RSpec.describe RegistrationsController do
end
end
+ context 'when the rate limit has been reached' do
+ it 'returns status 429 Too Many Requests', :aggregate_failures do
+ ip = '1.2.3.4'
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:user_sign_up, scope: ip).and_return(true)
+
+ controller.request.env['REMOTE_ADDR'] = ip
+ post(:create, params: user_params, session: session_params)
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+
it "logs a 'User Created' message" do
expect(Gitlab::AppLogger).to receive(:info).with(/\AUser Created: username=new_username email=new@user.com.+\z/).and_call_original
@@ -483,7 +499,7 @@ RSpec.describe RegistrationsController do
end
it 'succeeds if password is confirmed' do
- post :destroy, params: { password: '12345678' }
+ post :destroy, params: { password: Gitlab::Password.test_default }
expect_success
end
@@ -524,7 +540,7 @@ RSpec.describe RegistrationsController do
end
it 'fails' do
- delete :destroy, params: { password: '12345678' }
+ delete :destroy, params: { password: Gitlab::Password.test_default }
expect_failure(s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account'))
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index a54f16ec237..58d34a5e5c1 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -290,6 +290,14 @@ RSpec.describe SearchController do
expect(assigns[:search_objects].count).to eq(0)
end
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get(:show, params: { search: 'foo@bar.com', scope: 'users' })
+ end
+ end
end
describe 'GET #count', :aggregate_failures do
@@ -346,6 +354,14 @@ RSpec.describe SearchController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'count' => '0' })
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get(:count, params: { search: 'foo@bar.com', scope: 'users' })
+ end
+ end
end
describe 'GET #autocomplete' do
@@ -358,6 +374,14 @@ RSpec.describe SearchController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to match_array([])
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get(:autocomplete, params: { term: 'foo@bar.com', scope: 'users' })
+ end
+ end
end
describe '#append_info_to_payload' do
@@ -372,9 +396,10 @@ RSpec.describe SearchController do
expect(payload[:metadata]['meta.search.force_search_results']).to eq('true')
expect(payload[:metadata]['meta.search.filters.confidential']).to eq('true')
expect(payload[:metadata]['meta.search.filters.state']).to eq('true')
+ expect(payload[:metadata]['meta.search.project_ids']).to eq(%w(456 789))
end
- get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
+ get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', project_ids: %w(456 789), confidential: true, state: true, force_search_results: true }
end
it 'appends the default scope in meta.search.scope' do
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 558e68fbb8f..8e85e283b31 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -142,9 +142,12 @@ RSpec.describe Snippets::NotesController do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
end
- it_behaves_like 'request exceeding rate limit', :clean_gitlab_redis_cache do
- let(:params) { request_params }
- let(:request_full_path) { snippet_notes_path(public_snippet) }
+ it_behaves_like 'create notes request exceeding rate limit', :clean_gitlab_redis_cache do
+ let(:current_user) { user }
+
+ def request
+ post :create, params: request_params
+ end
end
end
@@ -170,9 +173,12 @@ RSpec.describe Snippets::NotesController do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
end
- it_behaves_like 'request exceeding rate limit', :clean_gitlab_redis_cache do
- let(:params) { request_params }
- let(:request_full_path) { snippet_notes_path(internal_snippet) }
+ it_behaves_like 'create notes request exceeding rate limit', :clean_gitlab_redis_cache do
+ let(:current_user) { user }
+
+ def request
+ post :create, params: request_params
+ end
end
end
@@ -239,10 +245,12 @@ RSpec.describe Snippets::NotesController do
expect { post :create, params: request_params }.to change { Note.count }.by(1)
end
- it_behaves_like 'request exceeding rate limit', :clean_gitlab_redis_cache do
- let(:params) { request_params }
- let(:request_full_path) { snippet_notes_path(private_snippet) }
- let(:user) { private_snippet.author }
+ it_behaves_like 'create notes request exceeding rate limit', :clean_gitlab_redis_cache do
+ let(:current_user) { private_snippet.author }
+
+ def request
+ post :create, params: request_params
+ end
end
end
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index edb412cbb9c..9bd6691bdb2 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -88,7 +88,8 @@ RSpec.describe 'Database schema' do
users_star_projects: %w[user_id],
vulnerability_identifiers: %w[external_id],
vulnerability_scanners: %w[external_id],
- security_scans: %w[pipeline_id] # foreign key is not added as ci_pipeline table will be moved into different db soon
+ security_scans: %w[pipeline_id], # foreign key is not added as ci_pipeline table will be moved into different db soon
+ vulnerability_reads: %w[cluster_agent_id]
}.with_indifferent_access.freeze
context 'for table' do
diff --git a/spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb b/spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb
deleted file mode 100644
index 815aaf7c397..00000000000
--- a/spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ChangeContinuousOnboardingLinkUrlsExperiment, :snowplow do
- before do
- stub_experiments(change_continuous_onboarding_link_urls: 'control')
- end
-
- describe '#track' do
- context 'when no namespace has been set' do
- it 'tracks the action as normal' do
- subject.track(:some_action)
-
- expect_snowplow_event(
- category: subject.name,
- action: 'some_action',
- namespace: nil,
- context: [
- {
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
- data: an_instance_of(Hash)
- }
- ]
- )
- end
- end
-
- context 'when a namespace has been set' do
- let_it_be(:namespace) { create(:namespace) }
-
- before do
- subject.namespace = namespace
- end
-
- it 'tracks the action and merges the namespace into the event args' do
- subject.track(:some_action)
-
- expect_snowplow_event(
- category: subject.name,
- action: 'some_action',
- namespace: namespace,
- context: [
- {
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
- data: an_instance_of(Hash)
- }
- ]
- )
- end
- end
- end
-end
diff --git a/spec/experiments/new_project_sast_enabled_experiment_spec.rb b/spec/experiments/new_project_sast_enabled_experiment_spec.rb
index 38f58c01973..041e5dfa469 100644
--- a/spec/experiments/new_project_sast_enabled_experiment_spec.rb
+++ b/spec/experiments/new_project_sast_enabled_experiment_spec.rb
@@ -4,7 +4,12 @@ require 'spec_helper'
RSpec.describe NewProjectSastEnabledExperiment do
it "defines the expected behaviors and variants" do
- expect(subject.behaviors.keys).to match_array(%w[control candidate free_indicator unchecked_candidate])
+ expect(subject.variant_names).to match_array([
+ :candidate,
+ :free_indicator,
+ :unchecked_candidate,
+ :unchecked_free_indicator
+ ])
end
it "publishes to the database" do
diff --git a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
new file mode 100644
index 00000000000..87417fe1637
--- /dev/null
+++ b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
+ subject(:experiment) { described_class.new(user: user) }
+
+ let_it_be(:user) { create(:user) }
+
+ describe '#candidate?' do
+ context 'when experiment subject is candidate' do
+ before do
+ stub_experiments(require_verification_for_namespace_creation: :candidate)
+ end
+
+ it 'returns true' do
+ expect(experiment.candidate?).to eq(true)
+ end
+ end
+
+ context 'when experiment subject is control' do
+ before do
+ stub_experiments(require_verification_for_namespace_creation: :control)
+ end
+
+ it 'returns false' do
+ expect(experiment.candidate?).to eq(false)
+ end
+ end
+ end
+
+ describe '#record_conversion' do
+ let_it_be(:namespace) { create(:namespace) }
+
+ context 'when should_track? is false' do
+ before do
+ allow(experiment).to receive(:should_track?).and_return(false)
+ end
+
+ it 'does not record a conversion event' do
+ expect(experiment.publish_to_database).to be_nil
+ expect(experiment.record_conversion(namespace)).to be_nil
+ end
+ end
+
+ context 'when should_track? is true' do
+ before do
+ allow(experiment).to receive(:should_track?).and_return(true)
+ end
+
+ it 'records a conversion event' do
+ experiment_subject = experiment.publish_to_database
+
+ expect { experiment.record_conversion(namespace) }.to change { experiment_subject.reload.converted_at }.from(nil)
+ .and change { experiment_subject.context }.to include('namespace_id' => namespace.id)
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index e6eaebc9b6b..011021f6320 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -338,6 +338,10 @@ FactoryBot.define do
running
runner factory: :ci_runner
+
+ after(:create) do |build|
+ build.create_runtime_metadata!
+ end
end
trait :artifacts do
@@ -596,6 +600,11 @@ FactoryBot.define do
failure_reason { 13 }
end
+ trait :deployment_rejected do
+ failed
+ failure_reason { 22 }
+ end
+
trait :with_runner_session do
after(:build) do |build|
build.build_runner_session(url: 'https://localhost')
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 223de873a04..e6eec280ed0 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -10,6 +10,10 @@ FactoryBot.define do
expire_at { Date.yesterday }
end
+ trait :locked do
+ locked { Ci::JobArtifact.lockeds[:artifacts_locked] }
+ end
+
trait :remote_store do
file_store { JobArtifactUploader::Store::REMOTE}
end
diff --git a/spec/factories/ci/pipeline_message.rb b/spec/factories/ci/pipeline_message.rb
new file mode 100644
index 00000000000..71fac24922d
--- /dev/null
+++ b/spec/factories/ci/pipeline_message.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_pipeline_message, class: 'Ci::PipelineMessage' do
+ pipeline factory: :ci_pipeline
+ content { 'warning' }
+ severity { 1 }
+ end
+end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index b2c1eff6fbd..122af139985 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -87,6 +87,10 @@ FactoryBot.define do
locked { Ci::Pipeline.lockeds[:unlocked] }
end
+ trait :artifacts_locked do
+ locked { Ci::Pipeline.lockeds[:artifacts_locked] }
+ end
+
trait :protected do
add_attribute(:protected) { true }
end
diff --git a/spec/factories/ci/secure_files.rb b/spec/factories/ci/secure_files.rb
new file mode 100644
index 00000000000..9198ea61d14
--- /dev/null
+++ b/spec/factories/ci/secure_files.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_secure_file, class: 'Ci::SecureFile' do
+ name { 'filename' }
+ file { fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks', 'application/octet-stream') }
+ checksum { 'foo1234' }
+ project
+ end
+end
diff --git a/spec/factories/clusters/agent_tokens.rb b/spec/factories/clusters/agent_tokens.rb
index c49d197c3cd..03f765123db 100644
--- a/spec/factories/clusters/agent_tokens.rb
+++ b/spec/factories/clusters/agent_tokens.rb
@@ -7,5 +7,9 @@ FactoryBot.define do
token_encrypted { Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(50)) }
sequence(:name) { |n| "agent-token-#{n}" }
+
+ trait :revoked do
+ status { :revoked }
+ end
end
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 29197768ec0..10fa739acc1 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -118,7 +118,6 @@ FactoryBot.define do
end
factory :clusters_applications_runner, class: 'Clusters::Applications::Runner' do
- runner factory: %i(ci_runner)
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
diff --git a/spec/factories/dependency_proxy.rb b/spec/factories/dependency_proxy.rb
index 836ee87e4d7..afa6c61116a 100644
--- a/spec/factories/dependency_proxy.rb
+++ b/spec/factories/dependency_proxy.rb
@@ -8,8 +8,8 @@ FactoryBot.define do
file_name { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz' }
status { :default }
- trait :expired do
- status { :expired }
+ trait :pending_destruction do
+ status { :pending_destruction }
end
end
@@ -22,8 +22,8 @@ FactoryBot.define do
content_type { 'application/vnd.docker.distribution.manifest.v2+json' }
status { :default }
- trait :expired do
- status { :expired }
+ trait :pending_destruction do
+ status { :pending_destruction }
end
end
end
diff --git a/spec/factories/group/crm_settings.rb b/spec/factories/group/crm_settings.rb
new file mode 100644
index 00000000000..06a31fd69c0
--- /dev/null
+++ b/spec/factories/group/crm_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :crm_settings, class: 'Group::CrmSettings' do
+ group
+ end
+end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 859f381e4c1..152ae061605 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -112,5 +112,11 @@ FactoryBot.define do
)
end
end
+
+ trait :crm_enabled do
+ after(:create) do |group|
+ create(:crm_settings, group: group, enabled: true)
+ end
+ end
end
end
diff --git a/spec/factories/incident_management/issuable_escalation_statuses.rb b/spec/factories/incident_management/issuable_escalation_statuses.rb
index 54d0887f386..0486e0481bf 100644
--- a/spec/factories/incident_management/issuable_escalation_statuses.rb
+++ b/spec/factories/incident_management/issuable_escalation_statuses.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :incident_management_issuable_escalation_status, class: 'IncidentManagement::IssuableEscalationStatus' do
- issue
+ association :issue, factory: :incident
triggered
trait :triggered do
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 76415f82ed0..f3a00ac083a 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :integration, aliases: [:service] do
+ factory :integration do
project
type { 'Integration' }
end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index a9a9416c48b..f0cef41db69 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -42,4 +42,6 @@ FactoryBot.define do
factory :group_label, traits: [:base_label] do
group
end
+
+ factory :admin_label, traits: [:base_label], class: 'Label'
end
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
index 2b3dabc07d8..e88bb634898 100644
--- a/spec/factories/namespaces.rb
+++ b/spec/factories/namespaces.rb
@@ -11,6 +11,14 @@ FactoryBot.define do
owner { association(:user, strategy: :build, namespace: instance, username: path) }
+ after(:create) do |namespace, evaluator|
+ # simulating ::Namespaces::ProcessSyncEventsWorker because most tests don't run Sidekiq inline
+ # Note: we need to get refreshed `traversal_ids` it is updated via SQL query
+ # in `Namespaces::Traversal::Linear#sync_traversal_ids` (see the NOTE in that method).
+ # We cannot use `.reload` because it cleans other on-the-fly attributes.
+ namespace.create_ci_namespace_mirror!(traversal_ids: Namespace.find(namespace.id).traversal_ids) unless namespace.ci_namespace_mirror
+ end
+
trait :with_aggregation_schedule do
after(:create) do |namespace|
create(:namespace_aggregation_schedules, namespace: namespace)
diff --git a/spec/factories/packages/package_files.rb b/spec/factories/packages/package_files.rb
index 845fd882beb..5eac0036b91 100644
--- a/spec/factories/packages/package_files.rb
+++ b/spec/factories/packages/package_files.rb
@@ -6,6 +6,8 @@ FactoryBot.define do
file_name { 'somefile.txt' }
+ status { :default }
+
transient do
file_fixture { 'spec/fixtures/packages/conan/recipe_files/conanfile.py' }
end
@@ -14,6 +16,10 @@ FactoryBot.define do
package_file.file = fixture_file_upload(evaluator.file_fixture)
end
+ trait :pending_destruction do
+ status { :pending_destruction }
+ end
+
factory :conan_package_file do
package { association(:conan_package, without_package_files: true) }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 981f10e8260..c345fa0c8b4 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -49,6 +49,8 @@ FactoryBot.define do
forward_deployment_enabled { nil }
restrict_user_defined_variables { nil }
ci_job_token_scope_enabled { nil }
+ runner_token_expiration_interval { nil }
+ runner_token_expiration_interval_human_readable { nil }
end
after(:build) do |project, evaluator|
@@ -92,6 +94,8 @@ FactoryBot.define do
project.keep_latest_artifact = evaluator.keep_latest_artifact unless evaluator.keep_latest_artifact.nil?
project.restrict_user_defined_variables = evaluator.restrict_user_defined_variables unless evaluator.restrict_user_defined_variables.nil?
project.ci_job_token_scope_enabled = evaluator.ci_job_token_scope_enabled unless evaluator.ci_job_token_scope_enabled.nil?
+ project.runner_token_expiration_interval = evaluator.runner_token_expiration_interval unless evaluator.runner_token_expiration_interval.nil?
+ project.runner_token_expiration_interval_human_readable = evaluator.runner_token_expiration_interval_human_readable unless evaluator.runner_token_expiration_interval_human_readable.nil?
if evaluator.import_status
import_state = project.import_state || project.build_import_state
@@ -101,6 +105,9 @@ FactoryBot.define do
import_state.last_error = evaluator.import_last_error
import_state.save!
end
+
+ # simulating ::Projects::ProcessSyncEventsWorker because most tests don't run Sidekiq inline
+ project.create_ci_project_mirror!(namespace_id: project.namespace_id) unless project.ci_project_mirror
end
trait :public do
diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb
index fc1f5d71f39..f00d1f8b808 100644
--- a/spec/factories/usage_data.rb
+++ b/spec/factories/usage_data.rb
@@ -19,16 +19,16 @@ FactoryBot.define do
create(:jira_import_state, :finished, project: projects[1], label: jira_label, imported_issues_count: 3)
create(:jira_import_state, :scheduled, project: projects[1], label: jira_label)
create(:prometheus_integration, project: projects[1])
- create(:service, project: projects[1], type: 'JenkinsService', active: true)
- create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
- create(:service, project: projects[1], type: 'SlackService', active: true)
- create(:service, project: projects[2], type: 'SlackService', active: true)
- create(:service, project: projects[2], type: 'MattermostService', active: false)
- create(:service, group: group, project: nil, type: 'MattermostService', active: true)
- mattermost_instance = create(:service, :instance, type: 'MattermostService', active: true)
- create(:service, project: projects[1], type: 'MattermostService', active: true, inherit_from_id: mattermost_instance.id)
- create(:service, group: group, project: nil, type: 'SlackService', active: true, inherit_from_id: mattermost_instance.id)
- create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true)
+ create(:integration, project: projects[1], type: 'JenkinsService', active: true)
+ create(:integration, project: projects[0], type: 'SlackSlashCommandsService', active: true)
+ create(:integration, project: projects[1], type: 'SlackService', active: true)
+ create(:integration, project: projects[2], type: 'SlackService', active: true)
+ create(:integration, project: projects[2], type: 'MattermostService', active: false)
+ create(:integration, group: group, project: nil, type: 'MattermostService', active: true)
+ mattermost_instance = create(:integration, :instance, type: 'MattermostService', active: true)
+ create(:integration, project: projects[1], type: 'MattermostService', active: true, inherit_from_id: mattermost_instance.id)
+ create(:integration, group: group, project: nil, type: 'SlackService', active: true, inherit_from_id: mattermost_instance.id)
+ create(:integration, project: projects[2], type: 'CustomIssueTrackerService', active: true)
create(:project_error_tracking_setting, project: projects[0])
create(:project_error_tracking_setting, project: projects[1], enabled: false)
alert_bot_issues = create_list(:incident, 2, project: projects[0], author: User.alert_bot)
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 8aa9654956e..5f325717ec5 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -5,7 +5,7 @@ FactoryBot.define do
email { generate(:email) }
name { generate(:name) }
username { generate(:username) }
- password { "12345678" }
+ password { Gitlab::Password.test_default }
role { 'software_developer' }
confirmed_at { Time.now }
confirmation_token { nil }
diff --git a/spec/factories/wikis.rb b/spec/factories/wikis.rb
index 05f6fb0de58..a357f4b448d 100644
--- a/spec/factories/wikis.rb
+++ b/spec/factories/wikis.rb
@@ -4,7 +4,7 @@ FactoryBot.define do
factory :wiki do
transient do
container { association(:project) }
- user { container.default_owner || association(:user) }
+ user { container.first_owner || association(:user) }
end
initialize_with { Wiki.for_container(container, user) }
diff --git a/spec/factories/work_item/work_item_types.rb b/spec/factories/work_items/work_item_types.rb
index 1c586aab59b..0920b36bcbd 100644
--- a/spec/factories/work_item/work_item_types.rb
+++ b/spec/factories/work_items/work_item_types.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :work_item_type, class: 'WorkItem::Type' do
+ factory :work_item_type, class: 'WorkItems::Type' do
namespace
name { generate(:work_item_type_name) }
- base_type { WorkItem::Type.base_types[:issue] }
+ base_type { WorkItems::Type.base_types[:issue] }
icon_name { 'issue-type-issue' }
initialize_with do
@@ -13,9 +13,9 @@ FactoryBot.define do
# Expect base_types to exist on the DB
if type_base_attributes.slice(:namespace, :namespace_id).compact.empty?
- WorkItem::Type.find_or_initialize_by(type_base_attributes).tap { |type| type.assign_attributes(attributes) }
+ WorkItems::Type.find_or_initialize_by(type_base_attributes).tap { |type| type.assign_attributes(attributes) }
else
- WorkItem::Type.new(attributes)
+ WorkItems::Type.new(attributes)
end
end
@@ -24,17 +24,17 @@ FactoryBot.define do
end
trait :incident do
- base_type { WorkItem::Type.base_types[:incident] }
+ base_type { WorkItems::Type.base_types[:incident] }
icon_name { 'issue-type-incident' }
end
trait :test_case do
- base_type { WorkItem::Type.base_types[:test_case] }
+ base_type { WorkItems::Type.base_types[:test_case] }
icon_name { 'issue-type-test-case' }
end
trait :requirement do
- base_type { WorkItem::Type.base_types[:requirement] }
+ base_type { WorkItems::Type.base_types[:requirement] }
icon_name { 'issue-type-requirements' }
end
end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index 9b74aa2ac5a..88b8fcd8d5e 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin deploy keys' do
+RSpec.describe 'admin deploy keys', :js do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
@@ -15,112 +15,81 @@ RSpec.describe 'admin deploy keys' do
gitlab_enable_admin_mode_sign_in(admin)
end
- shared_examples 'renders deploy keys correctly' do
- it 'show all public deploy keys' do
- visit admin_deploy_keys_path
+ it 'show all public deploy keys' do
+ visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content(deploy_key.title)
- expect(page).to have_content(another_deploy_key.title)
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content(deploy_key.title)
+ expect(page).to have_content(another_deploy_key.title)
end
+ end
- it 'shows all the projects the deploy key has write access' do
- write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
+ it 'shows all the projects the deploy key has write access' do
+ write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
- visit admin_deploy_keys_path
+ visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content(write_key.project.full_name)
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content(write_key.project.full_name)
end
+ end
- describe 'create a new deploy key' do
- let(:new_ssh_key) { attributes_for(:key)[:key] }
-
- before do
- visit admin_deploy_keys_path
- click_link 'New deploy key'
- end
-
- it 'creates a new deploy key' do
- fill_in 'deploy_key_title', with: 'laptop'
- fill_in 'deploy_key_key', with: new_ssh_key
- click_button 'Create'
-
- expect(current_path).to eq admin_deploy_keys_path
+ describe 'create a new deploy key' do
+ let(:new_ssh_key) { attributes_for(:key)[:key] }
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content('laptop')
- end
- end
+ before do
+ visit admin_deploy_keys_path
+ click_link 'New deploy key'
end
- describe 'update an existing deploy key' do
- before do
- visit admin_deploy_keys_path
- page.within('tr', text: deploy_key.title) do
- click_link(_('Edit deploy key'))
- end
- end
+ it 'creates a new deploy key' do
+ fill_in 'deploy_key_title', with: 'laptop'
+ fill_in 'deploy_key_key', with: new_ssh_key
+ click_button 'Create'
- it 'updates an existing deploy key' do
- fill_in 'deploy_key_title', with: 'new-title'
- click_button 'Save changes'
+ expect(current_path).to eq admin_deploy_keys_path
- expect(current_path).to eq admin_deploy_keys_path
-
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content('new-title')
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content('laptop')
end
end
end
- context 'when `admin_deploy_keys_vue` feature flag is enabled', :js do
- it_behaves_like 'renders deploy keys correctly'
-
- describe 'remove an existing deploy key' do
- before do
- visit admin_deploy_keys_path
+ describe 'update an existing deploy key' do
+ before do
+ visit admin_deploy_keys_path
+ page.within('tr', text: deploy_key.title) do
+ click_link(_('Edit deploy key'))
end
+ end
- it 'removes an existing deploy key' do
- accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do
- page.within('tr', text: deploy_key.title) do
- click_button _('Delete deploy key')
- end
- end
+ it 'updates an existing deploy key' do
+ fill_in 'deploy_key_title', with: 'new-title'
+ click_button 'Save changes'
- expect(current_path).to eq admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).not_to have_content(deploy_key.title)
- end
+ expect(current_path).to eq admin_deploy_keys_path
+
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content('new-title')
end
end
end
- context 'when `admin_deploy_keys_vue` feature flag is disabled' do
+ describe 'remove an existing deploy key' do
before do
- stub_feature_flags(admin_deploy_keys_vue: false)
+ visit admin_deploy_keys_path
end
- it_behaves_like 'renders deploy keys correctly'
-
- describe 'remove an existing deploy key' do
- before do
- visit admin_deploy_keys_path
- end
-
- it 'removes an existing deploy key' do
+ it 'removes an existing deploy key' do
+ accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do
page.within('tr', text: deploy_key.title) do
- click_link _('Remove deploy key')
+ click_button _('Delete deploy key')
end
+ end
- expect(current_path).to eq admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).not_to have_content(deploy_key.title)
- end
+ expect(current_path).to eq admin_deploy_keys_path
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).not_to have_content(deploy_key.title)
end
end
end
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index 86d60b5d483..ba0870a53ae 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'admin issues labels' do
+ include Spec::Support::Helpers::ModalHelpers
+
let!(:bug_label) { Label.create!(title: 'bug', template: true) }
let!(:feature_label) { Label.create!(title: 'feature', template: true) }
@@ -59,7 +61,7 @@ RSpec.describe 'admin issues labels' do
it 'creates new label' do
fill_in 'Title', with: 'support'
fill_in 'Background color', with: '#F95610'
- click_button 'Save'
+ click_button 'Create label'
page.within '.manage-labels-list' do
expect(page).to have_content('support')
@@ -69,7 +71,7 @@ RSpec.describe 'admin issues labels' do
it 'does not creates label with invalid color' do
fill_in 'Title', with: 'support'
fill_in 'Background color', with: '#12'
- click_button 'Save'
+ click_button 'Create label'
page.within '.label-form' do
expect(page).to have_content('Color must be a valid color code')
@@ -79,7 +81,7 @@ RSpec.describe 'admin issues labels' do
it 'does not creates label if label already exists' do
fill_in 'Title', with: 'bug'
fill_in 'Background color', with: '#F95610'
- click_button 'Save'
+ click_button 'Create label'
page.within '.label-form' do
expect(page).to have_content 'Title has already been taken'
@@ -93,11 +95,25 @@ RSpec.describe 'admin issues labels' do
fill_in 'Title', with: 'fix'
fill_in 'Background color', with: '#F15610'
- click_button 'Save'
+ click_button 'Save changes'
page.within '.manage-labels-list' do
expect(page).to have_content('fix')
end
end
+
+ it 'allows user to delete label', :js do
+ visit edit_admin_label_path(bug_label)
+
+ click_button 'Delete'
+
+ within_modal do
+ expect(page).to have_content("#{bug_label.title} will be permanently deleted. This cannot be undone.")
+
+ click_link 'Delete label'
+ end
+
+ expect(page).to have_content('Label was removed')
+ end
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index cc2d36221dc..ceb91b86876 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -21,12 +21,16 @@ RSpec.describe "Admin Runners" do
context "when there are runners" do
it 'has all necessary texts' do
- create(:ci_runner, :instance, contacted_at: Time.now)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.now)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago)
visit admin_runners_path
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online Runners 1"
+ expect(page).to have_text "Online runners 1"
+ expect(page).to have_text "Offline runners 2"
+ expect(page).to have_text "Stale runners 1"
end
it 'with an instance runner shows an instance badge' do
@@ -131,6 +135,9 @@ RSpec.describe "Admin Runners" do
it 'shows correct runner when description matches' do
input_filtered_search_keys('runner-foo')
+ expect(page).to have_link('All 1')
+ expect(page).to have_link('Instance 1')
+
expect(page).to have_content("runner-foo")
expect(page).not_to have_content("runner-bar")
end
@@ -138,71 +145,78 @@ RSpec.describe "Admin Runners" do
it 'shows no runner when description does not match' do
input_filtered_search_keys('runner-baz')
+ expect(page).to have_link('All 0')
+ expect(page).to have_link('Instance 0')
+
expect(page).to have_text 'No runners found'
end
end
describe 'filter by status' do
- it 'shows correct runner when status matches' do
- create(:ci_runner, :instance, description: 'runner-active', active: true)
- create(:ci_runner, :instance, description: 'runner-paused', active: false)
+ let!(:never_contacted) { create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) }
+
+ before do
+ create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.now)
+ create(:ci_runner, :instance, description: 'runner-2', contacted_at: Time.now)
+ create(:ci_runner, :instance, description: 'runner-paused', active: false, contacted_at: Time.now)
visit admin_runners_path
+ end
- expect(page).to have_content 'runner-active'
+ it 'shows all runners' do
+ expect(page).to have_content 'runner-1'
+ expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-paused'
+ expect(page).to have_content 'runner-never-contacted'
+ expect(page).to have_link('All 4')
+ end
+
+ it 'shows correct runner when status matches' do
input_filtered_search_filter_is_only('Status', 'Active')
- expect(page).to have_content 'runner-active'
+ expect(page).to have_link('All 3')
+
+ expect(page).to have_content 'runner-1'
+ expect(page).to have_content 'runner-2'
+ expect(page).to have_content 'runner-never-contacted'
expect(page).not_to have_content 'runner-paused'
end
it 'shows no runner when status does not match' do
- create(:ci_runner, :instance, description: 'runner-active', active: true)
- create(:ci_runner, :instance, description: 'runner-paused', active: false)
+ input_filtered_search_filter_is_only('Status', 'Stale')
- visit admin_runners_path
-
- input_filtered_search_filter_is_only('Status', 'Online')
-
- expect(page).not_to have_content 'runner-active'
- expect(page).not_to have_content 'runner-paused'
+ expect(page).to have_link('All 0')
expect(page).to have_text 'No runners found'
end
it 'shows correct runner when status is selected and search term is entered' do
- create(:ci_runner, :instance, description: 'runner-a-1', active: true)
- create(:ci_runner, :instance, description: 'runner-a-2', active: false)
- create(:ci_runner, :instance, description: 'runner-b-1', active: true)
-
- visit admin_runners_path
-
input_filtered_search_filter_is_only('Status', 'Active')
+ input_filtered_search_keys('runner-1')
- expect(page).to have_content 'runner-a-1'
- expect(page).to have_content 'runner-b-1'
- expect(page).not_to have_content 'runner-a-2'
-
- input_filtered_search_keys('runner-a')
+ expect(page).to have_link('All 1')
- expect(page).to have_content 'runner-a-1'
- expect(page).not_to have_content 'runner-b-1'
- expect(page).not_to have_content 'runner-a-2'
+ expect(page).to have_content 'runner-1'
+ expect(page).not_to have_content 'runner-2'
+ expect(page).not_to have_content 'runner-never-contacted'
+ expect(page).not_to have_content 'runner-paused'
end
- it 'shows correct runner when type is selected and search term is entered' do
- create(:ci_runner, :instance, description: 'runner-connected', contacted_at: Time.now)
- create(:ci_runner, :instance, description: 'runner-not-connected', contacted_at: nil)
+ it 'shows correct runner when status filter is entered' do
+ # use the string "Never" to avoid using space and trigger an early selection
+ input_filtered_search_filter_is_only('Status', 'Never')
- visit admin_runners_path
+ expect(page).to have_link('All 1')
- # use the string "Not" to avoid using space and trigger an early selection
- input_filtered_search_filter_is_only('Status', 'Not')
+ expect(page).not_to have_content 'runner-1'
+ expect(page).not_to have_content 'runner-2'
+ expect(page).not_to have_content 'runner-paused'
+ expect(page).to have_content 'runner-never-contacted'
- expect(page).not_to have_content 'runner-connected'
- expect(page).to have_content 'runner-not-connected'
+ within "[data-testid='runner-row-#{never_contacted.id}']" do
+ expect(page).to have_selector '.badge', text: 'never contacted'
+ end
end
end
@@ -215,6 +229,10 @@ RSpec.describe "Admin Runners" do
it '"All" tab is selected by default' do
visit admin_runners_path
+ expect(page).to have_link('All 2')
+ expect(page).to have_link('Group 1')
+ expect(page).to have_link('Project 1')
+
page.within('[data-testid="runner-type-tabs"]') do
expect(page).to have_link('All', class: 'active')
end
@@ -373,9 +391,28 @@ RSpec.describe "Admin Runners" do
it 'has all necessary texts including no runner message' do
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online Runners 0"
+
+ expect(page).to have_text "Online runners 0"
+ expect(page).to have_text "Offline runners 0"
+ expect(page).to have_text "Stale runners 0"
+
expect(page).to have_text 'No runners found'
end
+
+ it 'shows tabs with total counts equal to 0' do
+ expect(page).to have_link('All 0')
+ expect(page).to have_link('Instance 0')
+ expect(page).to have_link('Group 0')
+ expect(page).to have_link('Project 0')
+ end
+ end
+
+ context "when visiting outdated URLs" do
+ it 'updates NOT_CONNECTED runner status to NEVER_CONNECTED' do
+ visit admin_runners_path('status[]': 'NOT_CONNECTED')
+
+ expect(page).to have_current_path(admin_runners_path('status[]': 'NEVER_CONTACTED') )
+ end
end
describe 'runners registration' do
@@ -422,7 +459,9 @@ RSpec.describe "Admin Runners" do
before do
click_on 'Reset registration token'
- page.accept_alert
+ within_modal do
+ click_button('OK', match: :first)
+ end
wait_for_requests
end
@@ -437,26 +476,29 @@ RSpec.describe "Admin Runners" do
end
end
- describe "Runner show page" do
+ describe "Runner edit page" do
let(:runner) { create(:ci_runner) }
before do
@project1 = create(:project)
@project2 = create(:project)
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
+
+ wait_for_requests
end
describe 'runner page breadcrumbs' do
- it 'contains the current runner token' do
+ it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
- expect(page.find('h2')).to have_content(runner.short_sha)
+ expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
+ expect(page.find('h2')).to have_content("Edit")
end
end
end
- describe 'runner page title', :js do
- it 'contains the runner id' do
- expect(find('.page-title')).to have_content("Runner ##{runner.id}")
+ describe 'runner header', :js do
+ it 'contains the runner status, type and id' do
+ expect(page).to have_content("never contacted shared Runner ##{runner.id} created")
end
end
@@ -498,7 +540,7 @@ RSpec.describe "Admin Runners" do
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
before do
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it_behaves_like 'assignable runner'
@@ -508,7 +550,7 @@ RSpec.describe "Admin Runners" do
let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) }
before do
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it_behaves_like 'assignable runner'
@@ -519,7 +561,7 @@ RSpec.describe "Admin Runners" do
before do
@project1.destroy!
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it_behaves_like 'assignable runner'
@@ -530,7 +572,7 @@ RSpec.describe "Admin Runners" do
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
before do
- visit admin_runner_path(runner)
+ visit edit_admin_runner_path(runner)
end
it 'removed specific runner from project' do
@@ -567,6 +609,8 @@ RSpec.describe "Admin Runners" do
page.find('input').send_keys(search_term)
click_on 'Search'
end
+
+ wait_for_requests
end
def input_filtered_search_filter_is_only(filter, value)
@@ -583,5 +627,7 @@ RSpec.describe "Admin Runners" do
click_on 'Search'
end
+
+ wait_for_requests
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 29323c604ef..e136ab41966 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -275,7 +275,7 @@ RSpec.describe 'Admin updates settings' do
it 'enable hiding third party offers' do
page.within('.as-third-party-offers') do
- check 'Do not display offers from third parties'
+ check 'Do not display content for customer experience improvement and offers from third parties'
click_button 'Save changes'
end
@@ -530,6 +530,7 @@ RSpec.describe 'Admin updates settings' do
it 'loads usage ping payload on click', :js do
stub_usage_data_connections
+ stub_database_flavor_check
page.within('#js-usage-settings') do
expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 2b627707ff2..95e3f5c70e5 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -57,4 +57,33 @@ RSpec.describe "Admin::Users" do
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
end
end
+
+ describe 'prompt user about registration features' do
+ let(:message) { s_("RegistrationFeatures|Want to %{feature_title} for free?") % { feature_title: s_('RegistrationFeatures|send emails to users') } }
+
+ it 'does not render registration features CTA when service ping is enabled' do
+ stub_application_setting(usage_ping_enabled: true)
+
+ visit admin_users_path
+
+ expect(page).not_to have_content(message)
+ end
+
+ context 'with no license and service ping disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+
+ if Gitlab.ee?
+ allow(License).to receive(:current).and_return(nil)
+ end
+ end
+
+ it 'renders registration features CTA' do
+ visit admin_users_path
+
+ expect(page).to have_content(message)
+ expect(page).to have_link(s_('RegistrationFeatures|Registration Features Program'))
+ end
+ end
+ end
end
diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
index 22a27b33671..793a5bced00 100644
--- a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -19,4 +19,19 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ
expect(page).to have_link('Settings', href: edit_path)
expect(page).to have_link('Projects using custom settings', href: overrides_path)
end
+
+ it 'does not render integration form element' do
+ expect(page).not_to have_selector('[data-testid="integration-form"]')
+ end
+
+ context 'when `vue_integration_form` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_integration_form: false)
+ visit_instance_integration('Mattermost slash commands')
+ end
+
+ it 'renders integration form element' do
+ expect(page).to have_selector('[data-testid="integration-form"]')
+ end
+ end
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index ae940fecabe..0d053329627 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -125,6 +125,26 @@ RSpec.describe 'Admin::Users::User' do
end
end
+ context 'when a user is locked', time_travel_to: '2020-02-02 10:30:45 -0700' do
+ let_it_be(:locked_user) { create(:user, locked_at: DateTime.parse('2020-02-02 10:30:00 -0700')) }
+
+ before do
+ visit admin_user_path(locked_user)
+ end
+
+ it "displays `(Locked)` next to user's name" do
+ expect(page).to have_content("#{locked_user.name} (Locked)")
+ end
+
+ it 'allows a user to be unlocked from the `User administration dropdown', :js do
+ accept_gl_confirm("Unlock user #{locked_user.name}?", button_text: 'Unlock') do
+ click_action_in_user_dropdown(locked_user.id, 'Unlock')
+ end
+
+ expect(page).not_to have_content("#{locked_user.name} (Locked)")
+ end
+ end
+
describe 'Impersonation' do
let_it_be(:another_user) { create(:user) }
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index fa943245fcb..473f51370b3 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -462,9 +462,9 @@ RSpec.describe 'Admin::Users' do
visit projects_admin_user_path(user)
end
- it 'lists group projects' do
+ it 'lists groups' do
within(:css, '.gl-mb-3 + .card') do
- expect(page).to have_content 'Group projects'
+ expect(page).to have_content 'Groups'
expect(page).to have_link group.name, href: admin_group_path(group)
end
end
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 25e474bb676..49375e4b37b 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -34,7 +34,9 @@ RSpec.describe 'Issue board filters', :js do
it 'and submit one as filter', :aggregate_failures do
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2)
- expect_filtered_search_dropdown_results(filter_dropdown, 3)
+ wait_for_requests
+
+ expect_filtered_search_dropdown_results(filter_dropdown, 4)
click_on user.username
filter_submit.click
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 2f21961d1fc..d25cddea902 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -528,7 +528,7 @@ RSpec.describe 'Project issue boards', :js do
end
it 'does not allow dragging' do
- expect(page).not_to have_selector('.user-can-drag')
+ expect(page).not_to have_selector('.gl-cursor-grab')
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 0bb8e0bcdc0..0e914ae19d1 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -6,9 +6,11 @@ RSpec.describe 'Project issue boards sidebar', :js do
include BoardHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :public) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:board) { create(:board, project: project) }
- let_it_be(:list) { create(:list, board: board, position: 0) }
+ let_it_be(:label) { create(:label, project: project, name: 'Label') }
+ let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 4378e88f7c1..e600a99e3b6 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -30,10 +30,10 @@ RSpec.describe 'Commits' do
project.add_reporter(user)
end
- describe 'Commit builds with jobs_tab_feature flag off' do
+ describe 'Commit builds with jobs_tab_vue feature flag off' do
before do
stub_feature_flags(jobs_tab_vue: false)
- visit pipeline_path(pipeline)
+ visit builds_project_pipeline_path(project, pipeline)
end
it { expect(page).to have_content pipeline.sha[0..7] }
@@ -45,6 +45,23 @@ RSpec.describe 'Commits' do
end
end
end
+
+ describe 'Commit builds with jobs_tab_vue feature flag on', :js do
+ before do
+ visit builds_project_pipeline_path(project, pipeline)
+
+ wait_for_requests
+ end
+
+ it { expect(page).to have_content pipeline.sha[0..7] }
+
+ it 'contains generic commit status build' do
+ page.within('[data-testid="jobs-tab-table"]') do
+ expect(page).to have_content "##{status.id}" # build id
+ expect(page).to have_content 'generic' # build name
+ end
+ end
+ end
end
context 'commit status is Ci Build' do
@@ -103,6 +120,18 @@ RSpec.describe 'Commits' do
end
end
+ context 'Download artifacts with jobs_tab_vue feature flag on', :js do
+ before do
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
+ end
+
+ it do
+ visit builds_project_pipeline_path(project, pipeline)
+ wait_for_requests
+ expect(page).to have_link('Download artifacts', href: download_project_job_artifacts_path(project, build, file_type: :archive))
+ end
+ end
+
describe 'Cancel all builds' do
it 'cancels commit', :js, :sidekiq_might_not_need_inline do
visit pipeline_path(pipeline)
@@ -141,6 +170,27 @@ RSpec.describe 'Commits' do
end
end
+ context "when logged as reporter and with jobs_tab_vue feature flag on", :js do
+ before do
+ project.add_reporter(user)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
+ visit builds_project_pipeline_path(project, pipeline)
+ wait_for_requests
+ end
+
+ it 'renders header' do
+ expect(page).to have_content pipeline.sha[0..7]
+ expect(page).to have_content pipeline.git_commit_message.gsub!(/\s+/, ' ')
+ expect(page).to have_content pipeline.user.name
+ expect(page).not_to have_link('Cancel running')
+ expect(page).not_to have_link('Retry')
+ end
+
+ it do
+ expect(page).to have_link('Download artifacts')
+ end
+ end
+
context 'when accessing internal project with disallowed access', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299575' do
before do
project.update!(
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index a9fb6a2ae7e..64181041be5 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe 'Dashboard Issues' do
find('#select2-drop-mask', visible: false)
execute_script("$('#select2-drop-mask').remove();")
- find('.new-project-item-link').click
+ find('.js-new-project-item-link').click
expect(page).to have_current_path("#{project_path}/-/issues/new")
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 1ba16bf879a..9758454ab61 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Dashboard > Milestones' do
first('.select2-result-label').click
end
- find('.new-project-item-link').click
+ find('.js-new-project-item-link').click
expect(current_path).to eq(new_group_milestone_path(group))
end
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 7345bfa19e2..b00bdeac3b9 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -5,10 +5,11 @@ require 'spec_helper'
RSpec.describe 'Dashboard Todos' do
include DesignManagementTestHelpers
- let_it_be(:user) { create(:user, username: 'john') }
- let_it_be(:author) { create(:user) }
+ let_it_be(:user) { create(:user, username: 'john') }
+ let_it_be(:user2) { create(:user, username: 'diane') }
+ let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
- let_it_be(:issue) { create(:issue, project: project, due_date: Date.today, title: "Fix bug") }
+ let_it_be(:issue) { create(:issue, project: project, due_date: Date.today, title: "Fix bug") }
before_all do
project.add_developer(user)
@@ -23,6 +24,19 @@ RSpec.describe 'Dashboard Todos' do
it 'shows "All done" message' do
expect(page).to have_content 'Your To-Do List shows what to work on next'
end
+
+ context 'when user was assigned to an issue and marked it as done' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows "Are you looking for things to do?" message' do
+ create(:todo, :assigned, :done, user: user, project: project, target: issue, author: user2)
+ visit dashboard_todos_path
+
+ expect(page).to have_content 'Are you looking for things to do? Take a look at open issues, contribute to a merge request, or mention someone in a comment to automatically assign them a new to-do item.'
+ end
+ end
end
context 'when the todo references a merge request' do
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index 9fa77d5917d..f6821ae66e8 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe 'Dashboard > User filters projects' do
sorting_dropdown.click
- ['Last updated', 'Created date', 'Name', 'Stars'].each do |label|
+ ['Updated date', 'Created date', 'Name', 'Stars'].each do |label|
expect(sorting_dropdown).to have_content(label)
end
end
@@ -192,9 +192,9 @@ RSpec.describe 'Dashboard > User filters projects' do
end
end
- context 'Sorting by Last updated' do
+ context 'Sorting by Updated date' do
it 'sorts the project list' do
- select_dropdown_option '#filtered-search-sorting-dropdown', 'Last updated'
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Updated date'
expect_to_see_projects(desc_sorted_project_names)
diff --git a/spec/features/graphiql_spec.rb b/spec/features/graphiql_spec.rb
index 91f53b4bb7c..7729cdaa362 100644
--- a/spec/features/graphiql_spec.rb
+++ b/spec/features/graphiql_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'GraphiQL' do
end
it 'has the correct graphQLEndpoint' do
- expect(page.body).to include('var graphQLEndpoint = "/api/graphql";')
+ expect(page.body).to include('<div id="graphiql-container" data-graphql-endpoint-path="/api/graphql"')
end
end
@@ -26,7 +26,7 @@ RSpec.describe 'GraphiQL' do
end
it 'has the correct graphQLEndpoint' do
- expect(page.body).to include('var graphQLEndpoint = "/gitlab/root/api/graphql";')
+ expect(page.body).to include('<div id="graphiql-container" data-graphql-endpoint-path="/gitlab/root/api/graphql"')
end
end
end
diff --git a/spec/features/groups/dependency_proxy_for_containers_spec.rb b/spec/features/groups/dependency_proxy_for_containers_spec.rb
index a4cd6d0f503..ae721e7b91f 100644
--- a/spec/features/groups/dependency_proxy_for_containers_spec.rb
+++ b/spec/features/groups/dependency_proxy_for_containers_spec.rb
@@ -81,28 +81,11 @@ RSpec.describe 'Group Dependency Proxy for containers', :js do
let!(:dependency_proxy_blob) { create(:dependency_proxy_blob, group: group) }
it_behaves_like 'responds with the file'
-
- context 'dependency_proxy_workhorse feature flag disabled' do
- before do
- stub_feature_flags({ dependency_proxy_workhorse: false })
- end
-
- it_behaves_like 'responds with the file'
- end
end
end
context 'when the blob must be downloaded' do
it_behaves_like 'responds with the file'
it_behaves_like 'caches the file'
-
- context 'dependency_proxy_workhorse feature flag disabled' do
- before do
- stub_feature_flags({ dependency_proxy_workhorse: false })
- end
-
- it_behaves_like 'responds with the file'
- it_behaves_like 'caches the file'
- end
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 1bac1bcdf5a..3fc1484826c 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -156,10 +156,10 @@ RSpec.describe 'Group issues page' do
expect(page).to have_selector('.manual-ordering')
end
- it 'each issue item has a user-can-drag css applied' do
+ it 'each issue item has a gl-cursor-grab css applied' do
visit issues_group_path(group, sort: 'relative_position')
- expect(page).to have_selector('.issue.user-can-drag', count: 3)
+ expect(page).to have_selector('.issue.gl-cursor-grab', count: 3)
end
it 'issues should be draggable and persist order' do
diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb
index 2be7f61eeb9..8e6560af352 100644
--- a/spec/features/groups/labels/edit_spec.rb
+++ b/spec/features/groups/labels/edit_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Edit group label' do
+ include Spec::Support::Helpers::ModalHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:label) { create(:group_label, group: group) }
@@ -20,4 +22,16 @@ RSpec.describe 'Edit group label' do
expect(current_path).to eq(root_path)
expect(label.reload.title).to eq('new label name')
end
+
+ it 'allows user to delete label', :js do
+ click_button 'Delete'
+
+ within_modal do
+ expect(page).to have_content("#{label.title} will be permanently deleted from #{group.name}. This cannot be undone.")
+
+ click_link 'Delete label'
+ end
+
+ expect(page).to have_content("#{label.title} deleted permanently")
+ end
end
diff --git a/spec/features/groups/labels/sort_labels_spec.rb b/spec/features/groups/labels/sort_labels_spec.rb
index b5657db23cb..df75ff7c3cb 100644
--- a/spec/features/groups/labels/sort_labels_spec.rb
+++ b/spec/features/groups/labels/sort_labels_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Sort labels', :js do
expect(sort_options[1]).to eq('Name, descending')
expect(sort_options[2]).to eq('Last created')
expect(sort_options[3]).to eq('Oldest created')
- expect(sort_options[4]).to eq('Last updated')
+ expect(sort_options[4]).to eq('Updated date')
expect(sort_options[5]).to eq('Oldest updated')
click_link 'Name, descending'
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 077f680629f..7541e54f014 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Group merge requests page' do
end
it 'shows projects only with merge requests feature enabled', :js do
- find('.new-project-item-link').click
+ find('.js-new-project-item-link').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index da8032dc4dd..c5d2f5e6733 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe 'Group navbar' do
include_context 'group navbar structure'
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+
+ let(:group) { create(:group) }
before do
insert_package_nav(_('Kubernetes'))
@@ -40,7 +41,9 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
- context 'when customer_relations feature flag is enabled' do
+ context 'when customer_relations feature and flag is enabled' do
+ let(:group) { create(:group, :crm_enabled) }
+
before do
stub_feature_flags(customer_relations: true)
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 3c2ade6b274..26338b03349 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -42,6 +42,9 @@ RSpec.describe 'Group Packages' do
let_it_be(:maven_package) { create(:maven_package, project: second_project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') }
let_it_be(:packages) { [npm_package, maven_package] }
+ let(:package) { packages.first }
+ let(:package_details_path) { group_package_path(group, package) }
+
it_behaves_like 'packages list', check_project_name: true
it_behaves_like 'package details link'
diff --git a/spec/features/groups/settings/access_tokens_spec.rb b/spec/features/groups/settings/access_tokens_spec.rb
new file mode 100644
index 00000000000..20787c4c2f5
--- /dev/null
+++ b/spec/features/groups/settings/access_tokens_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Group > Settings > Access Tokens', :js do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:resource_settings_access_tokens_path) { group_settings_access_tokens_path(group) }
+
+ before_all do
+ group.add_owner(user)
+ end
+
+ before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
+ sign_in(user)
+ end
+
+ def create_resource_access_token
+ group.add_maintainer(bot_user)
+
+ create(:personal_access_token, user: bot_user)
+ end
+
+ context 'when user is not a group owner' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it_behaves_like 'resource access tokens missing access rights'
+ end
+
+ describe 'token creation' do
+ it_behaves_like 'resource access tokens creation', 'group'
+
+ context 'when token creation is not allowed' do
+ it_behaves_like 'resource access tokens creation disallowed', 'Group access token creation is disabled in this group. You can still use and manage existing tokens.'
+ end
+ end
+
+ describe 'active tokens' do
+ let!(:resource_access_token) { create_resource_access_token }
+
+ it_behaves_like 'active resource access tokens'
+ end
+
+ describe 'inactive tokens' do
+ let!(:resource_access_token) { create_resource_access_token }
+
+ it_behaves_like 'inactive resource access tokens', 'This group has no active access tokens.'
+ end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 9c11b84fa8f..19f60ce55d3 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -171,6 +171,28 @@ RSpec.describe 'Group' do
expect(page).not_to have_css('.recaptcha')
end
end
+
+ describe 'showing personalization questions on group creation when it is enabled' do
+ before do
+ stub_application_setting(hide_third_party_offers: false)
+ visit new_group_path(anchor: 'create-group-pane')
+ end
+
+ it 'renders personalization questions' do
+ expect(page).to have_content('Now, personalize your GitLab experience')
+ end
+ end
+
+ describe 'not showing personalization questions on group creation when it is enabled' do
+ before do
+ stub_application_setting(hide_third_party_offers: true)
+ visit new_group_path(anchor: 'create-group-pane')
+ end
+
+ it 'does not render personalization questions' do
+ expect(page).not_to have_content('Now, personalize your GitLab experience')
+ end
+ end
end
describe 'create a nested group', :js do
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
new file mode 100644
index 00000000000..db98f58240d
--- /dev/null
+++ b/spec/features/help_dropdown_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Help Dropdown", :js do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+
+ before do
+ stub_application_setting(version_check_enabled: true)
+ end
+
+ context 'when logged in as non-admin' do
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ it 'does not render version data' do
+ page.within '.header-help' do
+ find('.header-help-dropdown-toggle').click
+
+ expect(page).not_to have_text('Your GitLab Version')
+ expect(page).not_to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}")
+ expect(page).not_to have_selector('.version-check-badge')
+ expect(page).not_to have_text('Up to date')
+ end
+ end
+ end
+
+ context 'when logged in as admin' do
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
+
+ describe 'does render version data' do
+ where(:response, :ui_text) do
+ [
+ [{ "severity" => "success" }, 'Up to date'],
+ [{ "severity" => "warning" }, 'Update available'],
+ [{ "severity" => "danger" }, 'Update ASAP']
+ ]
+ end
+
+ with_them do
+ before do
+ allow_next_instance_of(VersionCheck) do |instance|
+ allow(instance).to receive(:response).and_return(response)
+ end
+ visit root_path
+ end
+
+ it 'renders correct version badge variant' do
+ page.within '.header-help' do
+ find('.header-help-dropdown-toggle').click
+
+ expect(page).to have_text('Your GitLab Version')
+ expect(page).to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}")
+ expect(page).to have_selector('.version-check-badge')
+ expect(page).to have_text(ui_text)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index a1e2990202c..546257b9f10 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -28,21 +28,20 @@ RSpec.describe 'Help Pages' do
end
end
- context 'in a production environment with version check enabled' do
+ describe 'with version check enabled' do
+ let_it_be(:user) { create(:user) }
+
before do
stub_application_setting(version_check_enabled: true)
+ allow(User).to receive(:single_user).and_return(double(user, requires_usage_stats_consent?: false))
+ allow(user).to receive(:can_read_all_resources?).and_return(true)
- stub_rails_env('production')
- allow(VersionCheck).to receive(:image_url).and_return('/version-check-url')
-
- sign_in(create(:user))
+ sign_in(user)
visit help_path
end
- it 'has a version check image' do
- # Check `data-src` due to lazy image loading
- expect(find('.js-version-status-badge', visible: false)['data-src'])
- .to end_with('/version-check-url')
+ it 'renders the version check badge' do
+ expect(page).to have_selector('.js-gitlab-version-check')
end
end
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
index f646cdbd71b..bc40fb713ac 100644
--- a/spec/features/issuables/sorting_list_spec.rb
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -54,10 +54,10 @@ RSpec.describe 'Sort Issuable List' do
context 'in the "merge requests / merged" tab', :js do
let(:issuable_type) { :merged_merge_request }
- it 'is "last updated"' do
+ it 'is "updated date"' do
visit_merge_requests_with_state(project, 'merged')
- expect(page).to have_button 'Last updated'
+ expect(page).to have_button 'Updated date'
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
end
@@ -66,10 +66,10 @@ RSpec.describe 'Sort Issuable List' do
context 'in the "merge requests / closed" tab', :js do
let(:issuable_type) { :closed_merge_request }
- it 'is "last updated"' do
+ it 'is "updated date"' do
visit_merge_requests_with_state(project, 'closed')
- expect(page).to have_button 'Last updated'
+ expect(page).to have_button 'Updated date'
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
end
@@ -95,7 +95,7 @@ RSpec.describe 'Sort Issuable List' do
visit_merge_requests_with_state(project, 'open')
click_button('Created date')
- click_link('Last updated')
+ click_link('Updated date')
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
@@ -152,10 +152,10 @@ RSpec.describe 'Sort Issuable List' do
context 'in the "issues / closed" tab', :js do
let(:issuable_type) { :closed_issue }
- it 'is "last updated"' do
+ it 'is "updated date"' do
visit_issues_with_state(project, 'closed')
- expect(page).to have_button 'Last updated'
+ expect(page).to have_button 'Updated date'
expect(first_issue).to include(last_updated_issuable.title)
expect(last_issue).to include(first_updated_issuable.title)
end
@@ -195,7 +195,7 @@ RSpec.describe 'Sort Issuable List' do
visit_issues_with_state(project, 'opened')
click_button('Created date')
- click_on('Last updated')
+ click_on('Updated date')
expect(page).to have_css('.issue:first-child', text: last_updated_issuable.title)
expect(page).to have_css('.issue:last-child', text: first_updated_issuable.title)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 9da6694c681..868946814c3 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -8,10 +8,9 @@ RSpec.describe 'Issue Sidebar' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:user) { create(:user) }
- let_it_be(:label) { create(:label, project: project, title: 'bug') }
- let_it_be(:issue) { create(:labeled_issue, project: project, labels: [label]) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:label) { create(:label, project: project, name: 'Label') }
let_it_be(:mock_date) { Date.today.at_beginning_of_month + 2.days }
- let_it_be(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
@@ -223,14 +222,6 @@ RSpec.describe 'Issue Sidebar' do
restore_window_size
open_issue_sidebar
end
-
- it 'escapes XSS when viewing issue labels' do
- page.within('.block.labels') do
- click_on 'Edit'
-
- expect(page).to have_content '<script>alert("xss");</script>'
- end
- end
end
context 'editing issue milestone', :js do
@@ -242,62 +233,7 @@ RSpec.describe 'Issue Sidebar' do
end
context 'editing issue labels', :js do
- before do
- issue.update!(labels: [label])
- page.within('.block.labels') do
- click_on 'Edit'
- end
- end
-
- it 'shows the current set of labels' do
- page.within('.issuable-show-labels') do
- expect(page).to have_content label.title
- end
- end
-
- it 'shows option to create a project label' do
- page.within('.block.labels') do
- expect(page).to have_content 'Create project'
- end
- end
-
- context 'creating a project label', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27992' do
- before do
- page.within('.block.labels') do
- click_link 'Create project'
- end
- end
-
- it 'shows dropdown switches to "create label" section' do
- page.within('.block.labels') do
- expect(page).to have_content 'Create project label'
- end
- end
-
- it 'adds new label' do
- page.within('.block.labels') do
- fill_in 'new_label_name', with: 'wontfix'
- page.find('.suggest-colors a', match: :first).click
- page.find('button', text: 'Create').click
-
- page.within('.dropdown-page-one') do
- expect(page).to have_content 'wontfix'
- end
- end
- end
-
- it 'shows error message if label title is taken' do
- page.within('.block.labels') do
- fill_in 'new_label_name', with: label.title
- page.find('.suggest-colors a', match: :first).click
- page.find('button', text: 'Create').click
-
- page.within('.dropdown-page-two') do
- expect(page).to have_content 'Title has already been taken'
- end
- end
- end
- end
+ it_behaves_like 'labels sidebar widget'
end
context 'interacting with collapsed sidebar', :js do
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index 0a879fdd4d4..cc0d35afd60 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
let_it_be(:support_bot) { User.support_bot }
before do
- stub_feature_flags(vue_issuables_list: true)
-
# The following two conditions equate to Gitlab::ServiceDesk.supported == true
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb
index 44c23813e3c..625303f89e4 100644
--- a/spec/features/issues/user_bulk_edits_issues_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_spec.rb
@@ -104,6 +104,26 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
end
end
+ describe 'select all issues' do
+ let!(:issue_2) { create(:issue, project: project) }
+
+ before do
+ stub_feature_flags(vue_issues_list: true)
+ end
+
+ it 'after selecting all issues, unchecking one issue only unselects that one issue' do
+ visit project_issues_path(project)
+
+ click_button 'Edit issues'
+ check 'Select all'
+ uncheck issue.title
+
+ expect(page).to have_unchecked_field 'Select all'
+ expect(page).to have_unchecked_field issue.title
+ expect(page).to have_checked_field issue_2.title
+ end
+ end
+
def create_closed
create(:issue, project: project, state: :closed)
end
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index 09d3ad15641..5d03aa1fc2b 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe "User comments on issue", :js do
before do
stub_feature_flags(tribute_autocomplete: false)
+ stub_feature_flags(sandboxed_mermaid: false)
project.add_guest(user)
sign_in(user)
@@ -49,7 +50,7 @@ RSpec.describe "User comments on issue", :js do
add_note(comment)
- expect(page.find('svg.mermaid')).to have_content html_content
+ expect(page.find('svg.mermaid')).not_to have_content 'javascript'
within('svg.mermaid') { expect(page).not_to have_selector('img') }
end
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 6e8b3e4fb7c..875b0a60634 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -217,7 +217,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
# Javascript debounces AJAX calls.
# So we have to wait until AJAX requests are started.
- # Details are in app/assets/javascripts/create_merge_request_dropdown.js
+ # Details are in app/assets/javascripts/issues/create_merge_request_dropdown.js
# this.refDebounce = _.debounce(...)
sleep 0.5
diff --git a/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb b/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
new file mode 100644
index 00000000000..1fa8f533869
--- /dev/null
+++ b/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User scrolls to deep-linked note' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:comment_1) { create(:note_on_issue, noteable: issue, project: project, note: 'written first') }
+ let_it_be(:comments) { create_list(:note_on_issue, 20, noteable: issue, project: project, note: 'spacer note') }
+
+ context 'on issue page', :js do
+ it 'on comment' do
+ visit project_issue_path(project, issue, anchor: "note_#{comment_1.id}")
+
+ wait_for_requests
+
+ expect(first_comment).to have_content(comment_1.note)
+
+ bottom_of_title = find('.issue-sticky-header.gl-fixed').evaluate_script("this.getBoundingClientRect().bottom;")
+ top = first_comment.evaluate_script("this.getBoundingClientRect().top;")
+
+ expect(top).to be_within(1).of(bottom_of_title)
+ end
+ end
+
+ def all_comments
+ all('.timeline > .note.timeline-entry')
+ end
+
+ def first_comment
+ all_comments.first
+ end
+end
diff --git a/spec/features/issues/user_sees_breadcrumb_links_spec.rb b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
index 9f8cd2a769d..669c7c45411 100644
--- a/spec/features/issues/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
@@ -8,8 +8,6 @@ RSpec.describe 'New issue breadcrumb' do
let(:user) { project.creator }
before do
- stub_feature_flags(vue_issuables_list: false)
-
sign_in(user)
visit(new_project_issue_path(project))
end
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index d3aaf339421..0e5a20fe24a 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe 'Copy as GFM', :js do
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
+ before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350454
+ end
+
describe 'Copying rendered GFM' do
before do
@feat = MarkdownFeature.new
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index e080c7ffb3f..6a91d4e03c1 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'Mermaid rendering', :js do
let_it_be(:project) { create(:project, :public) }
+ before do
+ stub_feature_flags(sandboxed_mermaid: false)
+ end
+
it 'renders Mermaid diagrams correctly' do
description = <<~MERMAID
```mermaid
diff --git a/spec/features/markdown/sandboxed_mermaid_spec.rb b/spec/features/markdown/sandboxed_mermaid_spec.rb
new file mode 100644
index 00000000000..f118fb3db66
--- /dev/null
+++ b/spec/features/markdown/sandboxed_mermaid_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Sandboxed Mermaid rendering', :js do
+ let_it_be(:project) { create(:project, :public) }
+
+ before do
+ stub_feature_flags(sandboxed_mermaid: true)
+ end
+
+ it 'includes mermaid frame correctly' do
+ description = <<~MERMAID
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+ MERMAID
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+
+ expected = %(<iframe src="/-/sandbox/mermaid" sandbox="allow-scripts" frameborder="0" scrolling="no")
+ expect(page.html).to include(expected)
+ 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 0117cf01e53..8761ee89463 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'view merge request with external CI service' do
before do
- create(:service, project: project,
+ create(:integration, project: project,
active: true,
type: 'DroneCiService',
category: 'ci')
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index 31b2b2d15aa..322ccc6a0c0 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -44,8 +44,8 @@ RSpec.describe 'Password reset' do
visit(edit_user_password_path(reset_password_token: token))
- fill_in 'New password', with: 'hello1234'
- fill_in 'Confirm new password', with: 'hello1234'
+ fill_in 'New password', with: "new" + Gitlab::Password.test_default
+ fill_in 'Confirm new password', with: "new" + Gitlab::Password.test_default
click_button 'Change your password'
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 24ba55994ae..34eb07d78f1 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Profile account page', :js do
it 'deletes user', :js, :sidekiq_might_not_need_inline do
click_button 'Delete account'
- fill_in 'password', with: '12345678'
+ fill_in 'password', with: Gitlab::Password.test_default
page.within '.modal' do
click_button 'Delete account'
@@ -62,66 +62,33 @@ RSpec.describe 'Profile account page', :js do
end
end
- describe 'when I reset feed token' do
- it 'resets feed token with `hide_access_tokens` feature flag enabled' do
- visit profile_personal_access_tokens_path
+ it 'allows resetting of feed token' do
+ visit profile_personal_access_tokens_path
- within('[data-testid="feed-token-container"]') do
- previous_token = find_field('Feed token').value
+ within('[data-testid="feed-token-container"]') do
+ previous_token = find_field('Feed token').value
- accept_confirm { click_link('reset this token') }
+ accept_confirm { click_link('reset this token') }
- click_button('Click to reveal')
+ click_button('Click to reveal')
- expect(find_field('Feed token').value).not_to eq(previous_token)
- end
- end
-
- it 'resets feed token with `hide_access_tokens` feature flag disabled' do
- stub_feature_flags(hide_access_tokens: false)
- visit profile_personal_access_tokens_path
-
- within('.feed-token-reset') do
- previous_token = find("#feed_token").value
-
- accept_confirm { find('[data-testid="reset_feed_token_link"]').click }
-
- expect(find('#feed_token').value).not_to eq(previous_token)
- end
+ expect(find_field('Feed token').value).not_to eq(previous_token)
end
end
- describe 'when I reset incoming email token' do
- before do
- allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- stub_feature_flags(bootstrap_confirmation_modals: false)
- end
-
- it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do
- visit profile_personal_access_tokens_path
-
- within('[data-testid="incoming-email-token-container"]') do
- previous_token = find_field('Incoming email token').value
-
- accept_confirm { click_link('reset this token') }
+ it 'allows resetting of incoming email token' do
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- click_button('Click to reveal')
+ visit profile_personal_access_tokens_path
- expect(find_field('Incoming email token').value).not_to eq(previous_token)
- end
- end
+ within('[data-testid="incoming-email-token-container"]') do
+ previous_token = find_field('Incoming email token').value
- it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do
- stub_feature_flags(hide_access_tokens: false)
- visit profile_personal_access_tokens_path
+ accept_confirm { click_link('reset this token') }
- within('.incoming-email-token-reset') do
- previous_token = find('#incoming_email_token').value
+ click_button('Click to reveal')
- accept_confirm { find('[data-testid="reset_email_token_link"]').click }
-
- expect(find('#incoming_email_token').value).not_to eq(previous_token)
- end
+ expect(find_field('Incoming email token').value).not_to eq(previous_token)
end
end
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
index 6270fa7347d..b392d8dfa8e 100644
--- a/spec/features/profiles/chat_names_spec.rb
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Profile > Chat' do
let(:user) { create(:user) }
- let(:integration) { create(:service) }
+ let(:integration) { create(:integration) }
before do
sign_in(user)
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
index 8f05de60be9..24917412826 100644
--- a/spec/features/profiles/emails_spec.rb
+++ b/spec/features/profiles/emails_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Profile > Emails' do
end
it 'does not add an invalid email' do
- fill_in('Email', with: 'test.@example.com')
+ fill_in('Email', with: 'test@@example.com')
click_button('Add email address')
email = user.emails.find_by(email: email)
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index c1e2d19ad9a..b9e59a0239b 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -32,10 +32,10 @@ RSpec.describe 'Profile > SSH Keys' do
expect(find('.breadcrumbs-sub-title')).to have_link(attrs[:title])
end
- it 'shows a confirmable warning if the key does not start with ssh-' do
+ it 'shows a confirmable warning if the key begins with an algorithm name that is unsupported' do
attrs = attributes_for(:key)
- fill_in('Key', with: 'invalid-key')
+ fill_in('Key', with: 'unsupported-ssh-rsa key')
fill_in('Title', with: attrs[:title])
click_button('Add key')
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 7059697354d..25fe43617fd 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Profile > Password' do
describe 'User puts the same passwords in the field and in the confirmation' do
it 'shows a success message' do
- fill_passwords('mypassword', 'mypassword')
+ fill_passwords(Gitlab::Password.test_default, Gitlab::Password.test_default)
page.within('.flash-notice') do
expect(page).to have_content('Password was successfully updated. Please sign in again.')
@@ -79,7 +79,7 @@ RSpec.describe 'Profile > Password' do
end
context 'Change password' do
- let(:new_password) { '22233344' }
+ let(:new_password) { "new" + Gitlab::Password.test_default }
before do
sign_in(user)
@@ -170,8 +170,8 @@ RSpec.describe 'Profile > Password' do
expect(current_path).to eq new_profile_password_path
fill_in :user_password, with: user.password
- fill_in :user_new_password, with: '12345678'
- fill_in :user_password_confirmation, with: '12345678'
+ fill_in :user_new_password, with: Gitlab::Password.test_default
+ fill_in :user_password_confirmation, with: Gitlab::Password.test_default
click_button 'Set new password'
expect(current_path).to eq new_user_session_path
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 135a940807e..f1e5658cd7b 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
describe "feed token" do
context "when enabled" do
- it "displays feed token with `hide_access_tokens` feature flag enabled" do
+ it "displays feed token" do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
@@ -143,15 +143,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
expect(page).to have_content(feed_token_description)
end
end
-
- it "displays feed token with `hide_access_tokens` feature flag disabled" do
- stub_feature_flags(hide_access_tokens: false)
- allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
- visit profile_personal_access_tokens_path
-
- expect(page).to have_field('Feed token', with: user.feed_token)
- expect(page).to have_content(feed_token_description)
- end
end
context "when disabled" do
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
index 1a368676a5e..11e2d24c36a 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -34,26 +34,23 @@ RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
end
it 'changes fragment hash if line number clicked' do
- ending_fragment = "L5"
-
visit_blob
find('#L3').click
- find("##{ending_fragment}").click
+ find("#L5").click
- expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: ending_fragment)))
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "LC5")))
end
it 'with initial fragment hash, changes fragment hash if line number clicked' do
fragment = "L1"
- ending_fragment = "L5"
visit_blob(fragment)
find('#L3').click
- find("##{ending_fragment}").click
+ find("#L5").click
- expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: ending_fragment)))
+ expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "LC5")))
end
end
@@ -73,26 +70,23 @@ RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
end
it 'changes fragment hash if line number clicked' do
- ending_fragment = "L5"
-
visit_blob
find('#L3').click
- find("##{ending_fragment}").click
+ find("#L5").click
- expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: ending_fragment)))
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "LC5")))
end
it 'with initial fragment hash, changes fragment hash if line number clicked' do
fragment = "L1"
- ending_fragment = "L5"
visit_blob(fragment)
find('#L3').click
- find("##{ending_fragment}").click
+ find("#L5").click
- expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: ending_fragment)))
+ expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "LC5")))
end
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 9d05c985af1..62994d19fc0 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -29,6 +29,10 @@ RSpec.describe 'File blob', :js do
).execute
end
+ before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350455
+ end
+
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')
diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb
index 8fc5c3d2e1b..0d08e7ea10d 100644
--- a/spec/features/projects/branches/user_deletes_branch_spec.rb
+++ b/spec/features/projects/branches/user_deletes_branch_spec.rb
@@ -32,28 +32,4 @@ RSpec.describe "User deletes branch", :js do
expect(page).to have_content('Branch was deleted')
end
-
- context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
- before do
- stub_feature_flags(bootstrap_confirmation_modals: false)
- stub_feature_flags(delete_branch_confirmation_modals: false)
- end
-
- it "deletes branch" do
- visit(project_branches_path(project))
-
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('improve/awesome')
- branch_search.native.send_keys(:enter)
-
- page.within(".js-branch-improve\\/awesome") do
- accept_alert { click_link(title: 'Delete branch') }
- end
-
- wait_for_requests
-
- expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
- end
- end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 2725c6a91be..363d08da024 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe 'Branches' do
it 'sorts the branches by name', :js do
visit project_branches_filtered_path(project, state: 'all')
- click_button "Last updated" # Open sorting dropdown
+ click_button "Updated date" # Open sorting dropdown
within '[data-testid="branches-dropdown"]' do
find('p', text: 'Name').click
end
@@ -128,7 +128,7 @@ RSpec.describe 'Branches' do
it 'sorts the branches by oldest updated', :js do
visit project_branches_filtered_path(project, state: 'all')
- click_button "Last updated" # Open sorting dropdown
+ click_button "Updated date" # Open sorting dropdown
within '[data-testid="branches-dropdown"]' do
find('p', text: 'Oldest updated').click
end
@@ -175,26 +175,6 @@ RSpec.describe 'Branches' do
expect(page).not_to have_content('fix')
expect(all('.all-branches').last).to have_selector('li', count: 0)
end
-
- context 'when the delete_branch_confirmation_modals feature flag is disabled' do
- it 'removes branch after confirmation', :js do
- stub_feature_flags(delete_branch_confirmation_modals: false)
- stub_feature_flags(bootstrap_confirmation_modals: false)
-
- visit project_branches_filtered_path(project, state: 'all')
-
- search_for_branch('fix')
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- accept_confirm do
- within('.js-branch-item', match: :first) { click_link(title: 'Delete branch') }
- end
-
- expect(page).not_to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 0)
- end
- end
end
context 'on project with 0 branch' do
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 6e88cbf52b5..0c9db24f1d8 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -219,7 +219,7 @@ RSpec.describe 'Gcp Cluster', :js do
it 'user does not see the offer' do
page.within('.as-third-party-offers') do
click_button 'Expand'
- check 'Do not display offers from third parties'
+ check 'Do not display content for customer experience improvement and offers from third parties'
click_button 'Save changes'
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index bcbf2f46f79..d88ff5c1aa5 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -342,24 +342,6 @@ RSpec.describe 'Environment' do
expect(page).not_to have_button('Stop')
end
- context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
- before do
- stub_feature_flags(delete_branch_confirmation_modals: false)
- end
-
- it 'user deletes the branch with running environment' do
- visit project_branches_filtered_path(project, state: 'all', search: 'feature')
-
- remove_branch_with_hooks(project, user, 'feature') do
- within('.js-branch-feature') { click_link(title: 'Delete branch') }
- end
-
- visit_environment(environment)
-
- expect(page).not_to have_button('Stop')
- end
- end
-
##
# This is a workaround for problem described in #24543
#
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index a7e773dda2d..23fcc1fe444 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Edit Project Settings' do
# disable by clicking toggle
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
@@ -32,7 +32,7 @@ RSpec.describe 'Edit Project Settings' do
# re-enable by clicking toggle again
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 4e9e129042c..508dec70db6 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -340,6 +340,7 @@ RSpec.describe "User browses files" do
let(:newrev) { project.repository.commit('master').sha }
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350456
create_file_in_repo(project, 'master', 'master', filename, 'Test file')
path = File.join('master', filename)
@@ -355,6 +356,7 @@ RSpec.describe "User browses files" do
context "when browsing a raw file" do
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350456
path = File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)
visit(project_blob_path(project, path))
diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb
index 3be5ab64834..17699847704 100644
--- a/spec/features/projects/files/user_browses_lfs_files_spec.rb
+++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'Projects > Files > User browses LFS files' do
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
expect(page).to have_content 'size 1575078'
- expect(page).not_to have_content 'Download (1.5 MB)'
+ expect(page).not_to have_content 'Download (1.50 MiB)'
end
end
@@ -56,7 +56,7 @@ RSpec.describe 'Projects > Files > User browses LFS files' do
click_link('lfs')
click_link('lfs_object.iso')
- expect(page).to have_content('Download (1.5 MB)')
+ expect(page).to have_content('Download (1.50 MiB)')
expect(page).not_to have_content('version https://git-lfs.github.com/spec/v1')
expect(page).not_to have_content('oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897')
expect(page).not_to have_content('size 1575078')
@@ -88,7 +88,7 @@ RSpec.describe 'Projects > Files > User browses LFS files' do
it 'does not show single file edit link' do
page.within('.content') do
expect(page).to have_selector(:link_or_button, 'Web IDE')
- expect(page).not_to have_selector(:link_or_button, 'Edit')
+ expect(page).not_to have_css('button[data-testid="edit"')
end
end
end
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index b6e300e9e59..c508b2ddba9 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/349953
sign_in(user)
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 453cc14c267..2b4ac3dc1d8 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
expect_fork_prompt
- click_link_or_button('Fork project')
+ click_link_or_button('Fork')
expect_fork_status
@@ -169,7 +169,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
expect_fork_prompt
- click_link_or_button('Fork project')
+ click_link_or_button('Fork')
expect_fork_status
@@ -183,7 +183,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
click_link_or_button('Edit')
expect_fork_prompt
- click_link_or_button('Fork project')
+ click_link_or_button('Fork')
find('.file-editor', match: :first)
@@ -214,7 +214,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
click_link('.gitignore')
click_link_or_button('Edit')
- expect(page).not_to have_link('Fork project')
+ expect(page).not_to have_link('Fork')
find('#editor')
set_editor_value('*.rbca')
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index c9b472260bd..fe9520fffc8 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/349953
sign_in(user)
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 3afd1937652..2fbec4e22f4 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -10,12 +10,11 @@ RSpec.describe 'Import/Export - project import integration test', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
before do
- stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
stub_uploads_object_storage(FileUploader)
allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(export_path)
end
- gitlab_sign_in(user)
+ sign_in(user)
end
after do
diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb
index 7a035248440..50010950f0e 100644
--- a/spec/features/projects/integrations/user_activates_jira_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jira_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'User activates Jira', :js do
it 'activates the Jira service' do
expect(page).to have_content('Jira settings saved and active.')
- expect(current_path).to eq(edit_project_service_path(project, :jira))
+ expect(current_path).to eq(edit_project_integration_path(project, :jira))
end
unless Gitlab.ee?
@@ -41,7 +41,7 @@ RSpec.describe 'User activates Jira', :js do
fill_in 'service_password', with: 'password'
click_test_integration
- page.within('.service-settings') do
+ page.within('[data-testid="integration-settings-form"]') do
expect(page).to have_content('This field is required.')
end
end
@@ -55,7 +55,7 @@ RSpec.describe 'User activates Jira', :js do
click_test_then_save_integration
expect(page).to have_content('Jira settings saved and active.')
- expect(current_path).to eq(edit_project_service_path(project, :jira))
+ expect(current_path).to eq(edit_project_integration_path(project, :jira))
end
end
end
@@ -72,7 +72,7 @@ RSpec.describe 'User activates Jira', :js do
it 'saves but does not activate the Jira service' do
expect(page).to have_content('Jira settings saved, but not active.')
- expect(current_path).to eq(edit_project_service_path(project, :jira))
+ expect(current_path).to eq(edit_project_integration_path(project, :jira))
end
it 'does not show the Jira link in the menu' do
diff --git a/spec/features/projects/labels/sort_labels_spec.rb b/spec/features/projects/labels/sort_labels_spec.rb
index 83559b816d2..26b3d08253c 100644
--- a/spec/features/projects/labels/sort_labels_spec.rb
+++ b/spec/features/projects/labels/sort_labels_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Sort labels', :js do
expect(sort_options[1]).to eq('Name, descending')
expect(sort_options[2]).to eq('Last created')
expect(sort_options[3]).to eq('Oldest created')
- expect(sort_options[4]).to eq('Last updated')
+ expect(sort_options[4]).to eq('Updated date')
expect(sort_options[5]).to eq('Oldest updated')
click_link 'Name, descending'
diff --git a/spec/features/projects/labels/user_edits_labels_spec.rb b/spec/features/projects/labels/user_edits_labels_spec.rb
index 8300a1a8542..999c238c7b3 100644
--- a/spec/features/projects/labels/user_edits_labels_spec.rb
+++ b/spec/features/projects/labels/user_edits_labels_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe "User edits labels" do
+ include Spec::Support::Helpers::ModalHelpers
+
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:user) { create(:user) }
@@ -24,4 +26,16 @@ RSpec.describe "User edits labels" do
expect(page).to have_content(new_title).and have_no_content(label.title)
end
end
+
+ it 'allows user to delete label', :js do
+ click_button 'Delete'
+
+ within_modal do
+ expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
+
+ click_link 'Delete label'
+ end
+
+ expect(page).to have_content('Label was removed')
+ end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 4dedd5689de..f1786c1be40 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -6,10 +6,6 @@ RSpec.describe 'New project', :js do
include Select2Helper
include Spec::Support::Helpers::Features::TopNavSpecHelpers
- before do
- stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
- end
-
context 'as a user' do
let(:user) { create(:user) }
@@ -179,7 +175,7 @@ RSpec.describe 'New project', :js do
it 'does not show the initialize with Readme checkbox on "Import project" tab' do
visit new_project_path
click_link 'Import project'
- first('.js-import-git-toggle-button').click
+ click_button 'Repo by URL'
page.within '#import-project-pane' do
expect(page).not_to have_css('input#project_initialize_with_readme')
@@ -196,9 +192,7 @@ RSpec.describe 'New project', :js do
end
it 'selects the user namespace' do
- page.within('#blank-project-pane') do
- expect(page).to have_select('project[namespace_id]', visible: false, selected: user.username)
- end
+ expect(page).to have_button user.username
end
end
@@ -212,9 +206,7 @@ RSpec.describe 'New project', :js do
end
it 'selects the group namespace' do
- page.within('#blank-project-pane') do
- expect(page).to have_select('project[namespace_id]', visible: false, selected: group.name)
- end
+ expect(page).to have_button group.name
end
end
@@ -229,9 +221,7 @@ RSpec.describe 'New project', :js do
end
it 'selects the group namespace' do
- page.within('#blank-project-pane') do
- expect(page).to have_select('project[namespace_id]', visible: false, selected: subgroup.full_path)
- end
+ expect(page).to have_button subgroup.full_path
end
end
@@ -249,22 +239,30 @@ RSpec.describe 'New project', :js do
end
it 'enables the correct visibility options' do
- select2(user.namespace_id, from: '#project_namespace_id')
+ click_button public_group.full_path
+ click_button user.username
+
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).not_to be_disabled
- select2(public_group.id, from: '#project_namespace_id')
+ click_button user.username
+ click_button public_group.full_path
+
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).not_to be_disabled
- select2(internal_group.id, from: '#project_namespace_id')
+ click_button public_group.full_path
+ click_button internal_group.full_path
+
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).to be_disabled
- select2(private_group.id, from: '#project_namespace_id')
+ click_button internal_group.full_path
+ click_button private_group.full_path
+
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).to be_disabled
expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).to be_disabled
@@ -355,9 +353,7 @@ RSpec.describe 'New project', :js do
end
it 'selects the group namespace' do
- page.within('#blank-project-pane') do
- expect(page).to have_select('project[namespace_id]', visible: false, selected: group.full_path)
- end
+ expect(page).to have_button group.full_path
end
end
end
diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb
index 7fcc8200b1c..8180f6b9aff 100644
--- a/spec/features/projects/packages_spec.rb
+++ b/spec/features/projects/packages_spec.rb
@@ -35,6 +35,9 @@ RSpec.describe 'Packages' do
let_it_be(:maven_package) { create(:maven_package, project: project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') }
let_it_be(:packages) { [npm_package, maven_package] }
+ let(:package) { packages.first }
+ let(:package_details_path) { project_package_path(project, package) }
+
it_behaves_like 'packages list'
it_behaves_like 'package details link'
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 6ddc8e43762..5176a7ec5a1 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1020,6 +1020,103 @@ RSpec.describe 'Pipeline', :js do
end
end
+ describe 'GET /:project/-/pipelines/:id/builds with jobs_tab_vue feature flag turned on' do
+ include_context 'pipeline builds'
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ visit builds_project_pipeline_path(project, pipeline)
+ end
+
+ it 'shows a list of jobs' do
+ expect(page).to have_content('Test')
+ expect(page).to have_content(build_passed.id)
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content(build_failed.id)
+ expect(page).to have_content(build_running.id)
+ expect(page).to have_content(build_external.id)
+ expect(page).to have_content('Retry')
+ expect(page).to have_content('Cancel running')
+ expect(page).to have_button('Play')
+ end
+
+ it 'shows jobs tab pane as active' do
+ expect(page).to have_css('#js-tab-builds.active')
+ end
+
+ context 'page tabs' do
+ it 'shows Pipeline, Jobs and DAG tabs with link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Jobs')
+ expect(page).to have_link('Needs')
+ end
+
+ it 'shows counter in Jobs tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
+ end
+
+ it 'shows Jobs tab as active' do
+ expect(page).to have_css('li.js-builds-tab-link .active')
+ end
+ end
+
+ context 'retrying jobs' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before do
+ find('[data-testid="retry"]', match: :first).click
+ end
+
+ it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
+ expect(page).not_to have_content('Retry')
+ end
+ end
+ end
+
+ context 'canceling jobs' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before do
+ click_on 'Cancel running'
+ end
+
+ it 'does not show a "Cancel running" button', :sidekiq_might_not_need_inline do
+ expect(page).not_to have_content('Cancel running')
+ end
+ end
+ end
+
+ context 'playing manual job' do
+ before do
+ within '[data-testid="jobs-tab-table"]' do
+ click_button('Play')
+
+ wait_for_requests
+ end
+ end
+
+ it { expect(build_manual.reload).to be_pending }
+ end
+
+ context 'when user unschedules a delayed job' do
+ before do
+ within '[data-testid="jobs-tab-table"]' do
+ click_button('Unschedule')
+ end
+ end
+
+ it 'unschedules the delayed job and shows play button as a manual job' do
+ expect(page).to have_button('Play')
+ expect(page).not_to have_button('Unschedule')
+ end
+ end
+ end
+
describe 'GET /:project/-/pipelines/:id/failures' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
index 019d50a497b..27c23e7beb5 100644
--- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'User activates issue tracker', :js do
it 'activates the service' do
expect(page).to have_content("#{tracker} settings saved and active.")
- expect(current_path).to eq(edit_project_service_path(project, tracker.parameterize(separator: '_')))
+ expect(current_path).to eq(edit_project_integration_path(project, tracker.parameterize(separator: '_')))
end
it 'shows the link in the menu' do
@@ -58,7 +58,7 @@ RSpec.describe 'User activates issue tracker', :js do
end
expect(page).to have_content("#{tracker} settings saved and active.")
- expect(current_path).to eq(edit_project_service_path(project, tracker.parameterize(separator: '_')))
+ expect(current_path).to eq(edit_project_integration_path(project, tracker.parameterize(separator: '_')))
end
end
end
@@ -73,7 +73,7 @@ RSpec.describe 'User activates issue tracker', :js do
it 'saves but does not activate the service' do
expect(page).to have_content("#{tracker} settings saved, but not active.")
- expect(current_path).to eq(edit_project_service_path(project, tracker.parameterize(separator: '_')))
+ expect(current_path).to eq(edit_project_integration_path(project, tracker.parameterize(separator: '_')))
end
it 'does not show the external tracker link in the menu' do
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index b2ca0424b6d..74919a99f04 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
let(:mattermost_enabled) { true }
describe 'activation' do
- let(:edit_path) { edit_project_service_path(project, :mattermost_slash_commands) }
+ let(:edit_path) { edit_project_integration_path(project, :mattermost_slash_commands) }
include_examples 'user activates the Mattermost Slash Command integration'
end
diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
index d5fe8b083ba..38b6ad84c77 100644
--- a/spec/features/projects/services/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'User activates Slack notifications', :js do
pipeline_channel: 6,
wiki_page_channel: 7)
- visit(edit_project_service_path(project, integration))
+ visit(edit_project_integration_path(project, integration))
end
it 'filters events by channel' do
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 bc84ccaa432..d46d1f739b7 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
@@ -24,7 +24,7 @@ RSpec.describe 'Slack slash commands', :js do
click_active_checkbox
click_on 'Save'
- expect(current_path).to eq(edit_project_service_path(project, :slack_slash_commands))
+ expect(current_path).to eq(edit_project_integration_path(project, :slack_slash_commands))
expect(page).to have_content('Slack slash commands settings saved, but not active.')
end
@@ -32,7 +32,7 @@ RSpec.describe 'Slack slash commands', :js do
fill_in 'Token', with: 'token'
click_on 'Save'
- expect(current_path).to eq(edit_project_service_path(project, :slack_slash_commands))
+ expect(current_path).to eq(edit_project_integration_path(project, :slack_slash_commands))
expect(page).to have_content('Slack slash commands settings saved and active.')
end
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index d8de9e0449e..122bf267021 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:resource_settings_access_tokens_path) { project_settings_access_tokens_path(project) }
before_all do
project.add_maintainer(user)
@@ -17,78 +18,25 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
sign_in(user)
end
- def create_project_access_token
+ def create_resource_access_token
project.add_maintainer(bot_user)
create(:personal_access_token, user: bot_user)
end
- def active_project_access_tokens
- find('.table.active-tokens')
- end
-
- def no_project_access_tokens_message
- find('.settings-message')
- end
-
- def created_project_access_token
- find('#created-personal-access-token').value
- end
-
context 'when user is not a project maintainer' do
before do
project.add_developer(user)
end
- it 'does not show project access token page' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_content("Page Not Found")
- end
+ it_behaves_like 'resource access tokens missing access rights'
end
describe 'token creation' do
- it 'allows creation of a project access token' do
- name = 'My project access token'
-
- visit project_settings_access_tokens_path(project)
- fill_in 'Token name', with: name
-
- # Set date to 1st of next month
- find_field('Expiration date').click
- find('.pika-next').click
- click_on '1'
-
- # Scopes
- check 'api'
- check 'read_api'
-
- click_on 'Create project access token'
-
- expect(active_project_access_tokens).to have_text(name)
- expect(active_project_access_tokens).to have_text('in')
- expect(active_project_access_tokens).to have_text('api')
- expect(active_project_access_tokens).to have_text('read_api')
- expect(active_project_access_tokens).to have_text('Maintainer')
- expect(created_project_access_token).not_to be_empty
- end
+ it_behaves_like 'resource access tokens creation', 'project'
context 'when token creation is not allowed' do
- before do
- group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
- end
-
- it 'does not show project access token creation form' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).not_to have_selector('#new_project_access_token')
- end
-
- it 'shows project access token creation disabled text' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_text('Project access token creation is disabled in this group. You can still use and manage existing tokens.')
- end
+ it_behaves_like 'resource access tokens creation disallowed', 'Project access token creation is disabled in this group. You can still use and manage existing tokens.'
context 'with a project in a personal namespace' do
let(:personal_project) { create(:project) }
@@ -97,113 +45,25 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
personal_project.add_maintainer(user)
end
- it 'shows project access token creation form and text' do
+ it 'shows access token creation form and text' do
visit project_settings_access_tokens_path(personal_project)
- expect(page).to have_selector('#new_project_access_token')
+ expect(page).to have_selector('#new_resource_access_token')
expect(page).to have_text('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.')
end
end
-
- context 'group settings link' do
- context 'when user is not a group owner' do
- before do
- group.add_developer(user)
- end
-
- it 'does not show group settings link' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).not_to have_link('group settings', href: edit_group_path(group))
- end
- end
-
- context 'with nested groups' do
- let(:subgroup) { create(:group, parent: group) }
-
- context 'when user is not a top level group owner' do
- before do
- subgroup.add_owner(user)
- end
-
- it 'does not show group settings link' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).not_to have_link('group settings', href: edit_group_path(group))
- end
- end
- end
-
- context 'when user is a group owner' do
- before do
- group.add_owner(user)
- end
-
- it 'shows group settings link' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_link('group settings', href: edit_group_path(group))
- end
- end
- end
end
end
describe 'active tokens' do
- let!(:project_access_token) { create_project_access_token }
+ let!(:resource_access_token) { create_resource_access_token }
- it 'shows active project access tokens' do
- visit project_settings_access_tokens_path(project)
-
- expect(active_project_access_tokens).to have_text(project_access_token.name)
- end
-
- context 'when User#time_display_relative is false' do
- before do
- user.update!(time_display_relative: false)
- end
-
- it 'shows absolute times for expires_at' do
- visit project_settings_access_tokens_path(project)
-
- expect(active_project_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
- end
- end
+ it_behaves_like 'active resource access tokens'
end
describe 'inactive tokens' do
- let!(:project_access_token) { create_project_access_token }
-
- no_active_tokens_text = 'This project has no active access tokens.'
+ let!(:resource_access_token) { create_resource_access_token }
- it 'allows revocation of an active token' do
- visit project_settings_access_tokens_path(project)
- accept_confirm { click_on 'Revoke' }
-
- expect(page).to have_selector('.settings-message')
- expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
- end
-
- it 'removes expired tokens from active section' do
- project_access_token.update!(expires_at: 5.days.ago)
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_selector('.settings-message')
- expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
- end
-
- context 'when resource access token creation is not allowed' do
- before do
- group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
- end
-
- it 'allows revocation of an active token' do
- visit project_settings_access_tokens_path(project)
- accept_confirm { click_on 'Revoke' }
-
- expect(page).to have_selector('.settings-message')
- expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
- end
- end
+ it_behaves_like 'inactive resource access tokens', 'This project has no active access tokens.'
end
end
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
index 71b319d192c..b67caa5a5f9 100644
--- a/spec/features/projects/settings/project_settings_spec.rb
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Projects settings' do
# disable by clicking toggle
forking_enabled_button.click
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
@@ -77,7 +77,7 @@ RSpec.describe 'Projects settings' do
expect(default_award_emojis_input.value).to eq('false')
page.within('.sharing-permissions') do
- find('input[value="Save changes"]').click
+ find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 862bae45fc6..77be351f3d8 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
- find('input[value="Save changes"]').send_keys(:return)
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).not_to have_content 'Pipelines must succeed'
@@ -74,7 +74,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
- find('input[value="Save changes"]').send_keys(:return)
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).to have_content 'Pipelines must succeed'
@@ -95,7 +95,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
- find('input[value="Save changes"]').send_keys(:return)
+ find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).to have_content 'Pipelines must succeed'
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index dc551158895..89f6b4237a4 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -316,7 +316,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
page.within('.project-buttons') do
- expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
+ expect(page).to have_link('Add Kubernetes cluster', href: project_clusters_path(project))
end
end
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index 345d16982fd..68fed9b8a74 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -5,14 +5,6 @@ require 'spec_helper'
RSpec.describe 'User changes public project visibility', :js do
include ProjectForksHelper
- before do
- fork_project(project, project.owner)
-
- sign_in(project.owner)
-
- visit edit_project_path(project)
- end
-
shared_examples 'changing visibility to private' do
it 'requires confirmation' do
visibility_select = first('.project-feature-controls .select-control')
@@ -22,7 +14,7 @@ RSpec.describe 'User changes public project visibility', :js do
click_button 'Save changes'
end
- find('.js-legacy-confirm-danger-input').send_keys(project.path_with_namespace)
+ fill_in 'confirm_name_input', with: project.path_with_namespace
page.within '.modal' do
click_button 'Reduce project visibility'
@@ -34,15 +26,85 @@ RSpec.describe 'User changes public project visibility', :js do
end
end
- context 'when a project is public' do
+ shared_examples 'does not require confirmation' do
+ it 'saves without confirmation' do
+ visibility_select = first('.project-feature-controls .select-control')
+ visibility_select.select('Private')
+
+ page.within('#js-shared-permissions') do
+ click_button 'Save changes'
+ end
+
+ wait_for_requests
+
+ expect(project.reload).to be_private
+ end
+ end
+
+ context 'when the project has forks' do
+ before do
+ fork_project(project, project.owner)
+
+ sign_in(project.owner)
+
+ visit edit_project_path(project)
+ end
+
+ context 'when a project is public' do
+ let(:project) { create(:project, :empty_repo, :public) }
+
+ it_behaves_like 'changing visibility to private'
+ end
+
+ context 'when the project is internal' do
+ let(:project) { create(:project, :empty_repo, :internal) }
+
+ it_behaves_like 'changing visibility to private'
+ end
+
+ context 'when the visibility level is untouched' do
+ let(:project) { create(:project, :empty_repo, :public) }
+
+ it 'saves without confirmation' do
+ expect(page).to have_selector('.js-emails-disabled', visible: true)
+ find('.js-emails-disabled input[type="checkbox"]').click
+
+ page.within('#js-shared-permissions') do
+ click_button 'Save changes'
+ end
+
+ wait_for_requests
+
+ expect(project.reload).to be_public
+ end
+ end
+ end
+
+ context 'when the project is not forked' do
let(:project) { create(:project, :empty_repo, :public) }
- it_behaves_like 'changing visibility to private'
+ before do
+ sign_in(project.owner)
+
+ visit edit_project_path(project)
+ end
+
+ it_behaves_like 'does not require confirmation'
end
- context 'when the project is internal' do
- let(:project) { create(:project, :empty_repo, :internal) }
+ context 'with unlink_fork_network_upon_visibility_decrease = false' do
+ let(:project) { create(:project, :empty_repo, :public) }
+
+ before do
+ stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
+
+ fork_project(project, project.owner)
+
+ sign_in(project.owner)
+
+ visit edit_project_path(project)
+ end
- it_behaves_like 'changing visibility to private'
+ it_behaves_like 'does not require confirmation'
end
end
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 17c65e645f4..c4e2e3353a4 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe 'User creates a project', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
sign_in(user)
create(:personal_key, user: user)
end
@@ -44,9 +43,7 @@ RSpec.describe 'User creates a project', :js do
expect(page).to have_checked_field 'Initialize repository with a README'
expect(page).to have_checked_field 'Enable Static Application Security Testing (SAST)'
- page.within('#content-body') do
- click_button('Create project')
- end
+ click_button('Create project')
project = Project.last
@@ -96,12 +93,10 @@ RSpec.describe 'User creates a project', :js do
fill_in :project_name, with: 'A Subgroup Project'
fill_in :project_path, with: 'a-subgroup-project'
- page.find('.js-select-namespace').click
- page.find("div[role='option']", text: subgroup.full_path).click
+ click_button user.username
+ click_button subgroup.full_path
- page.within('#content-body') do
- click_button('Create project')
- end
+ click_button('Create project')
expect(page).to have_content("Project 'A Subgroup Project' was successfully created")
@@ -125,8 +120,8 @@ RSpec.describe 'User creates a project', :js do
fill_in :project_name, with: 'a-new-project'
fill_in :project_path, with: 'a-new-project'
- page.find('.js-select-namespace').click
- page.find("div[role='option']", text: group.full_path).click
+ click_button user.username
+ click_button group.full_path
page.within('#content-body') do
click_button('Create project')
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index 6a5ed49f1a6..71e43467a39 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -41,10 +41,10 @@ RSpec.describe 'User sorts projects and order persists' do
sign_in(user)
visit(explore_projects_path)
find('#sort-projects-dropdown').click
- first(:link, 'Last updated').click
+ first(:link, 'Updated date').click
end
- it_behaves_like "sort order persists across all views", "Last updated", "Last updated"
+ it_behaves_like "sort order persists across all views", 'Updated date', 'Updated date'
end
context 'from dashboard projects' do
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index d220db01c24..94085b075aa 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'View on environment', :js do
let(:user) { project.creator }
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350457
project.add_maintainer(user)
end
@@ -48,26 +49,6 @@ RSpec.describe 'View on environment', :js do
let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') }
let!(:deployment) { create(:deployment, :success, environment: environment, ref: branch_name, sha: sha) }
- context 'when visiting the diff of a merge request for the branch' do
- let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) }
-
- before do
- sign_in(user)
-
- visit diffs_project_merge_request_path(project, merge_request)
-
- wait_for_requests
- end
-
- it 'has a "View on env" button' do
- within '.diffs' do
- text = 'View on feature.review.example.com'
- url = 'http://feature.review.example.com/ruby/feature'
- expect(page).to have_selector("a[title='#{text}'][href='#{url}']")
- end
- end
- end
-
context 'when visiting a comparison for the branch' do
before do
sign_in(user)
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 15ec11c256f..4278efc5a8f 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -29,21 +29,6 @@ RSpec.describe 'Protected Branches', :js do
expect(page).to have_button('Only a project maintainer or owner can delete a protected branch', disabled: true)
end
-
- context 'when feature flag :delete_branch_confirmation_modals is disabled' do
- before do
- stub_feature_flags(delete_branch_confirmation_modals: false)
- end
-
- it 'does not allow developer to remove protected branch' do
- visit project_branches_path(project)
-
- find('input[data-testid="branch-search"]').set('fix')
- find('input[data-testid="branch-search"]').native.send_keys(:enter)
-
- expect(page).to have_selector('button[data-testid="remove-protected-branch"][disabled]')
- end
- end
end
end
@@ -79,32 +64,6 @@ RSpec.describe 'Protected Branches', :js do
expect(page).to have_content('No branches to show')
end
-
- context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
- before do
- stub_feature_flags(delete_branch_confirmation_modals: false)
- end
-
- it 'removes branch after modal confirmation' do
- visit project_branches_path(project)
-
- find('input[data-testid="branch-search"]').set('fix')
- find('input[data-testid="branch-search"]').native.send_keys(:enter)
-
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').click
-
- expect(page).to have_css('.js-delete-branch[disabled]')
- fill_in 'delete_branch_input', with: 'fix'
- click_link 'Delete protected branch'
-
- find('input[data-testid="branch-search"]').set('fix')
- find('input[data-testid="branch-search"]').native.send_keys(:enter)
-
- expect(page).to have_content('No branches to show')
- end
- end
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 22de77f7cd0..49c468976b9 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -268,10 +268,27 @@ RSpec.describe 'Runners' do
it 'group runners are not available' do
visit project_runners_path(project)
+ expect(page).not_to have_content 'Group owners can register group runners in the group\'s CI/CD settings.'
+ expect(page).to have_content 'Ask your group owner to set up a group runner'
+ end
+ end
+ end
+
+ context 'as project maintainer and group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ context 'project with a group but no group runner' do
+ let(:project) { create :project, group: group }
+
+ it 'group runners are available' do
+ visit project_runners_path(project)
+
expect(page).to have_content 'This group does not have any group runners yet.'
- expect(page).to have_content 'Group maintainers can register group runners in the group\'s CI/CD settings.'
- expect(page).not_to have_content 'Ask your group maintainer to set up a group runner'
+ expect(page).to have_content 'Group owners can register group runners in the group\'s CI/CD settings.'
+ expect(page).not_to have_content 'Ask your group owner to set up a group runner'
end
end
end
@@ -296,8 +313,8 @@ RSpec.describe 'Runners' do
expect(page).to have_content 'This group does not have any group runners yet.'
- expect(page).not_to have_content 'Group maintainers can register group runners in the group\'s CI/CD settings.'
- expect(page).to have_content 'Ask your group maintainer to set up a group runner.'
+ expect(page).not_to have_content 'Group owners can register group runners in the group\'s CI/CD settings.'
+ expect(page).to have_content 'Ask your group owner to set up a group runner.'
end
end
diff --git a/spec/features/user_sees_marketing_header_spec.rb b/spec/features/user_sees_marketing_header_spec.rb
new file mode 100644
index 00000000000..31f088ce010
--- /dev/null
+++ b/spec/features/user_sees_marketing_header_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe 'User sees experimental lmarketing header' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when not logged in' do
+ context 'when experiment candidate' do
+ it 'shows marketing header links', :aggregate_failures do
+ stub_experiments(logged_out_marketing_header: :candidate)
+
+ visit project_path(project)
+
+ expect(page).to have_text "About GitLab"
+ expect(page).to have_text "Pricing"
+ expect(page).to have_text "Talk to an expert"
+ expect(page).to have_text "Sign up now"
+ expect(page).to have_text "Login"
+ end
+ end
+
+ context 'when experiment candidate (trial focused variant)' do
+ it 'shows marketing header links', :aggregate_failures do
+ stub_experiments(logged_out_marketing_header: :trial_focused)
+
+ visit project_path(project)
+
+ expect(page).to have_text "About GitLab"
+ expect(page).to have_text "Pricing"
+ expect(page).to have_text "Talk to an expert"
+ expect(page).to have_text "Get a free trial"
+ expect(page).to have_text "Sign up"
+ expect(page).to have_text "Login"
+ end
+ end
+
+ context 'when experiment control' do
+ it 'does not show marketing header links', :aggregate_failures do
+ stub_experiments(logged_out_marketing_header: :control)
+
+ visit project_path(project)
+
+ expect(page).not_to have_text "About GitLab"
+ expect(page).not_to have_text "Pricing"
+ expect(page).not_to have_text "Talk to an expert"
+ expect(page).not_to have_text "Sign up now"
+ expect(page).not_to have_text "Login"
+ expect(page).not_to have_text "Get a free trial"
+ expect(page).not_to have_text "Sign up"
+ expect(page).to have_text "Sign in / Register"
+ end
+ end
+ end
+
+ context 'when logged in' do
+ it 'does not show marketing header links', :aggregate_failures do
+ sign_in(create(:user))
+
+ stub_experiments(logged_out_marketing_header: :candidate)
+
+ visit project_path(project)
+
+ expect(page).not_to have_text "About GitLab"
+ expect(page).not_to have_text "Pricing"
+ expect(page).not_to have_text "Talk to an expert"
+ end
+ end
+end
diff --git a/spec/features/user_sorts_things_spec.rb b/spec/features/user_sorts_things_spec.rb
index 6eaa620b538..8e6f6a96bd2 100644
--- a/spec/features/user_sorts_things_spec.rb
+++ b/spec/features/user_sorts_things_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe "User sorts things" do
end
it "issues -> project home page -> issues" do
- sort_option = "Last updated"
+ sort_option = 'Updated date'
visit(project_issues_path(project))
@@ -34,7 +34,7 @@ RSpec.describe "User sorts things" do
end
it "issues -> merge requests" do
- sort_option = "Last updated"
+ sort_option = 'Updated date'
visit(project_issues_path(project))
@@ -46,7 +46,7 @@ RSpec.describe "User sorts things" do
end
it "merge requests -> dashboard merge requests" do
- sort_option = "Last updated"
+ sort_option = 'Updated date'
visit(project_merge_requests_path(project))
diff --git a/spec/features/users/anonymous_sessions_spec.rb b/spec/features/users/anonymous_sessions_spec.rb
index 6b21412ae3d..f9b23626397 100644
--- a/spec/features/users/anonymous_sessions_spec.rb
+++ b/spec/features/users/anonymous_sessions_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state do
visit new_user_session_path
# The session key only gets created after a post
fill_in 'user_login', with: 'non-existant@gitlab.org'
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
expect(page).to have_content('Invalid login or password')
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 7ef11194ff9..2780549eea1 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -49,15 +49,15 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
expect(current_path).to eq edit_user_password_path
expect(page).to have_content('Please create a password for your new account.')
- fill_in 'user_password', with: 'password'
- fill_in 'user_password_confirmation', with: 'password'
+ fill_in 'user_password', with: Gitlab::Password.test_default
+ fill_in 'user_password_confirmation', with: Gitlab::Password.test_default
click_button 'Change your password'
expect(current_path).to eq new_user_session_path
expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
fill_in 'user_login', with: user.username
- fill_in 'user_password', with: 'password'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
expect_single_session_with_authenticated_ttl
@@ -210,7 +210,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
end
it 'does not allow sign-in if the user password is updated before entering a one-time code' do
- user.update!(password: 'new_password')
+ user.update!(password: "new" + Gitlab::Password.test_default)
enter_code(user.current_otp)
@@ -447,7 +447,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
expect(current_path).to eq(new_profile_password_path)
@@ -456,7 +456,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
end
context 'with invalid username and password' do
- let(:user) { create(:user, password: 'not-the-default') }
+ let(:user) { create(:user, password: "not" + Gitlab::Password.test_default) }
it 'blocks invalid login' do
expect(authentication_metrics)
@@ -767,7 +767,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
@@ -788,7 +788,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
@@ -809,7 +809,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
@@ -844,7 +844,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
fill_in 'user_otp_attempt', with: user.reload.current_otp
@@ -870,7 +870,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
visit new_user_session_path
fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
click_button 'Sign in'
expect_to_be_on_terms_page
@@ -878,7 +878,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
expect(current_path).to eq(new_profile_password_path)
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_password', with: Gitlab::Password.test_default
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
click_button 'Set new password'
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 7e3c1abd6d1..dac244e4300 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -206,7 +206,7 @@ RSpec.describe Ci::RunnersFinder do
sub_group_4.runners << runner_sub_group_4
end
- describe '#execute' do
+ shared_examples '#execute' do
subject { described_class.new(current_user: user, params: params).execute }
shared_examples 'membership equal to :descendants' do
@@ -349,6 +349,16 @@ RSpec.describe Ci::RunnersFinder do
end
end
+ it_behaves_like '#execute'
+
+ context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do
+ before do
+ stub_feature_flags(ci_find_runners_by_ci_mirrors: false)
+ end
+
+ it_behaves_like '#execute'
+ end
+
describe '#sort_key' do
subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key }
diff --git a/spec/finders/environments/environments_by_deployments_finder_spec.rb b/spec/finders/environments/environments_by_deployments_finder_spec.rb
index 1b86aced67d..8349092c79e 100644
--- a/spec/finders/environments/environments_by_deployments_finder_spec.rb
+++ b/spec/finders/environments/environments_by_deployments_finder_spec.rb
@@ -22,16 +22,6 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
create(:deployment, :success, environment: environment_two, ref: 'v1.1.0', tag: true, sha: project.commit('HEAD~1').id)
end
- it 'returns environment when with_tags is set' do
- expect(described_class.new(project, user, ref: 'master', commit: commit, with_tags: true).execute)
- .to contain_exactly(environment, environment_two)
- end
-
- it 'does not return environment when no with_tags is set' do
- expect(described_class.new(project, user, ref: 'master', commit: commit).execute)
- .to be_empty
- end
-
it 'does not return environment when commit is not part of deployment' do
expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
.to be_empty
@@ -41,7 +31,7 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
# This tests to ensure we don't call one CommitIsAncestor per environment
it 'only calls Gitaly twice when multiple environments are present', :request_store do
expect do
- result = described_class.new(project, user, ref: 'master', commit: commit, with_tags: true, find_latest: true).execute
+ result = described_class.new(project, user, ref: 'v1.1.0', commit: commit, find_latest: true).execute
expect(result).to contain_exactly(environment_two)
end.to change { Gitlab::GitalyClient.get_request_count }.by(2)
diff --git a/spec/finders/fork_targets_finder_spec.rb b/spec/finders/fork_targets_finder_spec.rb
index 12f01227af8..fe5b50ef030 100644
--- a/spec/finders/fork_targets_finder_spec.rb
+++ b/spec/finders/fork_targets_finder_spec.rb
@@ -16,7 +16,9 @@ RSpec.describe ForkTargetsFinder do
end
let!(:developer_group) do
- create(:group).tap { |g| g.add_developer(user) }
+ create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
+ g.add_developer(user)
+ end
end
let!(:reporter_group) do
@@ -33,11 +35,11 @@ RSpec.describe ForkTargetsFinder do
describe '#execute' do
it 'returns all user manageable namespaces' do
- expect(finder.execute).to match_array([user.namespace, maintained_group, owned_group, project.namespace])
+ expect(finder.execute).to match_array([user.namespace, maintained_group, owned_group, project.namespace, developer_group])
end
it 'returns only groups when only_groups option is passed' do
- expect(finder.execute(only_groups: true)).to match_array([maintained_group, owned_group, project.namespace])
+ expect(finder.execute(only_groups: true)).to match_array([maintained_group, owned_group, project.namespace, developer_group])
end
it 'returns groups relation when only_groups option is passed' do
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index f6b87f7eeab..59eeb078e9e 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -17,262 +17,250 @@ RSpec.describe GroupDescendantsFinder do
described_class.new(current_user: user, parent_group: group, params: params)
end
- shared_examples 'group descentants finder examples' do
- describe '#has_children?' do
+ describe '#has_children?' do
+ it 'is true when there are projects' do
+ create(:project, namespace: group)
+
+ expect(finder.has_children?).to be_truthy
+ end
+
+ context 'when there are subgroups' do
it 'is true when there are projects' do
- create(:project, namespace: group)
+ create(:group, parent: group)
expect(finder.has_children?).to be_truthy
end
+ end
+ end
- context 'when there are subgroups' do
- it 'is true when there are projects' do
- create(:group, parent: group)
+ describe '#execute' do
+ it 'includes projects' do
+ project = create(:project, namespace: group)
- expect(finder.has_children?).to be_truthy
- end
- end
+ expect(finder.execute).to contain_exactly(project)
end
- describe '#execute' do
- it 'includes projects' do
+ context 'when archived is `true`' do
+ let(:params) { { archived: 'true' } }
+
+ it 'includes archived projects' do
+ archived_project = create(:project, namespace: group, archived: true)
project = create(:project, namespace: group)
- expect(finder.execute).to contain_exactly(project)
+ expect(finder.execute).to contain_exactly(archived_project, project)
end
+ end
- context 'when archived is `true`' do
- let(:params) { { archived: 'true' } }
+ context 'when archived is `only`' do
+ let(:params) { { archived: 'only' } }
- it 'includes archived projects' do
- archived_project = create(:project, namespace: group, archived: true)
- project = create(:project, namespace: group)
+ it 'includes only archived projects' do
+ archived_project = create(:project, namespace: group, archived: true)
+ _project = create(:project, namespace: group)
- expect(finder.execute).to contain_exactly(archived_project, project)
- end
+ expect(finder.execute).to contain_exactly(archived_project)
end
+ end
- context 'when archived is `only`' do
- let(:params) { { archived: 'only' } }
+ it 'does not include archived projects' do
+ _archived_project = create(:project, :archived, namespace: group)
- it 'includes only archived projects' do
- archived_project = create(:project, namespace: group, archived: true)
- _project = create(:project, namespace: group)
+ expect(finder.execute).to be_empty
+ end
- expect(finder.execute).to contain_exactly(archived_project)
- end
- end
+ context 'with a filter' do
+ let(:params) { { filter: 'test' } }
- it 'does not include archived projects' do
- _archived_project = create(:project, :archived, namespace: group)
+ it 'includes only projects matching the filter' do
+ _other_project = create(:project, namespace: group)
+ matching_project = create(:project, namespace: group, name: 'testproject')
- expect(finder.execute).to be_empty
+ expect(finder.execute).to contain_exactly(matching_project)
end
+ end
- context 'with a filter' do
- let(:params) { { filter: 'test' } }
+ it 'sorts elements by name as default' do
+ project1 = create(:project, namespace: group, name: 'z')
+ project2 = create(:project, namespace: group, name: 'a')
- it 'includes only projects matching the filter' do
- _other_project = create(:project, namespace: group)
- matching_project = create(:project, namespace: group, name: 'testproject')
+ expect(subject.execute).to match_array([project2, project1])
+ end
- expect(finder.execute).to contain_exactly(matching_project)
- end
+ context 'sorting by name' do
+ let!(:project1) { create(:project, namespace: group, name: 'a', path: 'project-a') }
+ let!(:project2) { create(:project, namespace: group, name: 'z', path: 'project-z') }
+ let(:params) do
+ {
+ sort: 'name_asc'
+ }
end
- it 'sorts elements by name as default' do
- project1 = create(:project, namespace: group, name: 'z')
- project2 = create(:project, namespace: group, name: 'a')
-
- expect(subject.execute).to match_array([project2, project1])
+ it 'sorts elements by name' do
+ expect(subject.execute).to eq(
+ [
+ project1,
+ project2
+ ]
+ )
end
- context 'sorting by name' do
- let!(:project1) { create(:project, namespace: group, name: 'a', path: 'project-a') }
- let!(:project2) { create(:project, namespace: group, name: 'z', path: 'project-z') }
- let(:params) do
- {
- sort: 'name_asc'
- }
- end
+ context 'with nested groups' do
+ let!(:subgroup1) { create(:group, parent: group, name: 'a', path: 'sub-a') }
+ let!(:subgroup2) { create(:group, parent: group, name: 'z', path: 'sub-z') }
it 'sorts elements by name' do
expect(subject.execute).to eq(
[
+ subgroup1,
+ subgroup2,
project1,
project2
]
)
end
-
- context 'with nested groups' do
- let!(:subgroup1) { create(:group, parent: group, name: 'a', path: 'sub-a') }
- let!(:subgroup2) { create(:group, parent: group, name: 'z', path: 'sub-z') }
-
- it 'sorts elements by name' do
- expect(subject.execute).to eq(
- [
- subgroup1,
- subgroup2,
- project1,
- project2
- ]
- )
- end
- end
end
+ end
- it 'does not include projects shared with the group' do
- project = create(:project, namespace: group)
- other_project = create(:project)
- other_project.project_group_links.create!(group: group,
- group_access: Gitlab::Access::MAINTAINER)
+ it 'does not include projects shared with the group' do
+ project = create(:project, namespace: group)
+ other_project = create(:project)
+ other_project.project_group_links.create!(group: group,
+ group_access: Gitlab::Access::MAINTAINER)
- expect(finder.execute).to contain_exactly(project)
- end
+ expect(finder.execute).to contain_exactly(project)
end
+ end
- context 'with shared groups' do
- let_it_be(:other_group) { create(:group) }
- let_it_be(:shared_group_link) do
- create(:group_group_link,
- shared_group: group,
- shared_with_group: other_group)
- end
+ context 'with shared groups' do
+ let_it_be(:other_group) { create(:group) }
+ let_it_be(:shared_group_link) do
+ create(:group_group_link,
+ shared_group: group,
+ shared_with_group: other_group)
+ end
- context 'without common ancestor' do
+ context 'without common ancestor' do
+ it { expect(finder.execute).to be_empty }
+ end
+
+ context 'with common ancestor' do
+ let_it_be(:common_ancestor) { create(:group) }
+ let_it_be(:other_group) { create(:group, parent: common_ancestor) }
+ let_it_be(:group) { create(:group, parent: common_ancestor) }
+
+ context 'querying under the common ancestor' do
it { expect(finder.execute).to be_empty }
end
- context 'with common ancestor' do
- let_it_be(:common_ancestor) { create(:group) }
- let_it_be(:other_group) { create(:group, parent: common_ancestor) }
- let_it_be(:group) { create(:group, parent: common_ancestor) }
-
- context 'querying under the common ancestor' do
- it { expect(finder.execute).to be_empty }
+ context 'querying the common ancestor' do
+ subject(:finder) do
+ described_class.new(current_user: user, parent_group: common_ancestor, params: params)
end
- context 'querying the common ancestor' do
- subject(:finder) do
- described_class.new(current_user: user, parent_group: common_ancestor, params: params)
- end
-
- it 'contains shared subgroups' do
- expect(finder.execute).to contain_exactly(group, other_group)
- end
+ it 'contains shared subgroups' do
+ expect(finder.execute).to contain_exactly(group, other_group)
end
end
end
+ end
- context 'with nested groups' do
- let!(:project) { create(:project, namespace: group) }
- let!(:subgroup) { create(:group, :private, parent: group) }
+ context 'with nested groups' do
+ let!(:project) { create(:project, namespace: group) }
+ let!(:subgroup) { create(:group, :private, parent: group) }
- describe '#execute' do
- it 'contains projects and subgroups' do
- expect(finder.execute).to contain_exactly(subgroup, project)
- end
+ describe '#execute' do
+ it 'contains projects and subgroups' do
+ expect(finder.execute).to contain_exactly(subgroup, project)
+ end
- it 'does not include subgroups the user does not have access to' do
- subgroup.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ it 'does not include subgroups the user does not have access to' do
+ subgroup.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- public_subgroup = create(:group, :public, parent: group, path: 'public-group')
- other_subgroup = create(:group, :private, parent: group, path: 'visible-private-group')
- other_user = create(:user)
- other_subgroup.add_developer(other_user)
+ public_subgroup = create(:group, :public, parent: group, path: 'public-group')
+ other_subgroup = create(:group, :private, parent: group, path: 'visible-private-group')
+ other_user = create(:user)
+ other_subgroup.add_developer(other_user)
- finder = described_class.new(current_user: other_user, parent_group: group)
+ finder = described_class.new(current_user: other_user, parent_group: group)
- expect(finder.execute).to contain_exactly(public_subgroup, other_subgroup)
- end
+ expect(finder.execute).to contain_exactly(public_subgroup, other_subgroup)
+ end
- it 'only includes public groups when no user is given' do
- public_subgroup = create(:group, :public, parent: group)
- _private_subgroup = create(:group, :private, parent: group)
+ it 'only includes public groups when no user is given' do
+ public_subgroup = create(:group, :public, parent: group)
+ _private_subgroup = create(:group, :private, parent: group)
- finder = described_class.new(current_user: nil, parent_group: group)
+ finder = described_class.new(current_user: nil, parent_group: group)
- expect(finder.execute).to contain_exactly(public_subgroup)
- end
+ expect(finder.execute).to contain_exactly(public_subgroup)
+ end
- context 'when archived is `true`' do
- let(:params) { { archived: 'true' } }
+ context 'when archived is `true`' do
+ let(:params) { { archived: 'true' } }
- it 'includes archived projects in the count of subgroups' do
- create(:project, namespace: subgroup, archived: true)
+ it 'includes archived projects in the count of subgroups' do
+ create(:project, namespace: subgroup, archived: true)
- expect(finder.execute.first.preloaded_project_count).to eq(1)
- end
+ expect(finder.execute.first.preloaded_project_count).to eq(1)
end
+ end
- context 'with a filter' do
- let(:params) { { filter: 'test' } }
+ context 'with a filter' do
+ let(:params) { { filter: 'test' } }
- it 'contains only matching projects and subgroups' do
- matching_project = create(:project, namespace: group, name: 'Testproject')
- matching_subgroup = create(:group, name: 'testgroup', parent: group)
+ it 'contains only matching projects and subgroups' do
+ matching_project = create(:project, namespace: group, name: 'Testproject')
+ matching_subgroup = create(:group, name: 'testgroup', parent: group)
- expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
- end
+ expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
+ end
- it 'does not include subgroups the user does not have access to' do
- _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
- other_subgroup = create(:group, :private, parent: group, name: 'test2')
- public_subgroup = create(:group, :public, parent: group, name: 'test3')
- other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
- other_user = create(:user)
- other_subgroup.add_developer(other_user)
+ it 'does not include subgroups the user does not have access to' do
+ _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
+ other_subgroup = create(:group, :private, parent: group, name: 'test2')
+ public_subgroup = create(:group, :public, parent: group, name: 'test3')
+ other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
+ other_user = create(:user)
+ other_subgroup.add_developer(other_user)
- finder = described_class.new(current_user: other_user,
- parent_group: group,
- params: params)
+ finder = described_class.new(current_user: other_user,
+ parent_group: group,
+ params: params)
- expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
- end
+ expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
+ end
- context 'with matching children' do
- it 'includes a group that has a subgroup matching the query and its parent' do
- matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
+ context 'with matching children' do
+ it 'includes a group that has a subgroup matching the query and its parent' do
+ matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
- expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
+ end
- it 'includes the parent of a matching project' do
- matching_project = create(:project, namespace: subgroup, name: 'Testproject')
+ it 'includes the parent of a matching project' do
+ matching_project = create(:project, namespace: subgroup, name: 'Testproject')
- expect(finder.execute).to contain_exactly(subgroup, matching_project)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching_project)
+ end
- context 'with a small page size' do
- let(:params) { { filter: 'test', per_page: 1 } }
+ context 'with a small page size' do
+ let(:params) { { filter: 'test', per_page: 1 } }
- it 'contains all the ancestors of a matching subgroup regardless the page size' do
- subgroup = create(:group, :private, parent: group)
- matching = create(:group, :private, name: 'testgroup', parent: subgroup)
+ it 'contains all the ancestors of a matching subgroup regardless the page size' do
+ subgroup = create(:group, :private, parent: group)
+ matching = create(:group, :private, name: 'testgroup', parent: subgroup)
- expect(finder.execute).to contain_exactly(subgroup, matching)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching)
end
+ end
- it 'does not include the parent itself' do
- group.update!(name: 'test')
+ it 'does not include the parent itself' do
+ group.update!(name: 'test')
- expect(finder.execute).not_to include(group)
- end
+ expect(finder.execute).not_to include(group)
end
end
end
end
end
-
- it_behaves_like 'group descentants finder examples'
-
- context 'when feature flag :linear_group_descendants_finder is disabled' do
- before do
- stub_feature_flags(linear_group_descendants_finder: false)
- end
-
- it_behaves_like 'group descentants finder examples'
- end
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 0d797b7923c..a9a8e9d19b8 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -3,83 +3,93 @@
require 'spec_helper'
RSpec.describe GroupMembersFinder, '#execute' do
- let(:group) { create(:group) }
- let(:sub_group) { create(:group, parent: group) }
- let(:sub_sub_group) { create(:group, parent: sub_group) }
- let(:user1) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:user4) { create(:user) }
- let(:user5) { create(:user, :two_factor_via_otp) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: group) }
+ let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
+ let_it_be(:public_shared_group) { create(:group, :public) }
+ let_it_be(:private_shared_group) { create(:group, :private) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:user4) { create(:user) }
+ let_it_be(:user5) { create(:user, :two_factor_via_otp) }
+
+ let!(:link) do
+ create(:group_group_link, shared_group: group, shared_with_group: public_shared_group)
+ create(:group_group_link, shared_group: sub_group, shared_with_group: private_shared_group)
+ end
let(:groups) do
{
- group: group,
- sub_group: sub_group,
- sub_sub_group: sub_sub_group
+ group: group,
+ sub_group: sub_group,
+ sub_sub_group: sub_sub_group,
+ public_shared_group: public_shared_group,
+ private_shared_group: private_shared_group
}
end
context 'relations' do
let!(:members) do
{
- user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
- user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
- user1_group: create(:group_member, :reporter, group: group, user: user1),
- user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2),
- user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2),
- user2_group: create(:group_member, :maintainer, group: group, user: user2),
- user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now),
- user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now),
- user3_group: create(:group_member, :reporter, group: group, user: user3),
- user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4),
- user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now),
- user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now)
+ user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
+ user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
+ user1_group: create(:group_member, :reporter, group: group, user: user1),
+ user1_public_shared_group: create(:group_member, :maintainer, group: public_shared_group, user: user1),
+ user1_private_shared_group: create(:group_member, :maintainer, group: private_shared_group, user: user1),
+ user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2),
+ user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2),
+ user2_group: create(:group_member, :maintainer, group: group, user: user2),
+ user2_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user2),
+ user2_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user2),
+ user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now),
+ user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now),
+ user3_group: create(:group_member, :reporter, group: group, user: user3),
+ user3_public_shared_group: create(:group_member, :reporter, group: public_shared_group, user: user3),
+ user3_private_shared_group: create(:group_member, :reporter, group: private_shared_group, user: user3),
+ user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4),
+ user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now),
+ user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now),
+ user4_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user4),
+ user4_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user4)
}
end
it 'raises an error if a non-supported relation type is used' do
expect do
described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type])
- end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants.")
+ end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants, shared_from_groups.")
end
using RSpec::Parameterized::TableSyntax
where(:subject_relations, :subject_group, :expected_members) do
- nil | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:inherited] | :group | []
- [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
- [:direct, :inherited] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:direct, :descendants] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
- [:direct, :descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
- nil | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
- [:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
- [:direct, :inherited] | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct, :descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
- [:descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_sub_group, :user4_group]
- [:direct, :descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
- nil | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
- [:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:descendants] | :sub_sub_group | []
- [:direct, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct, :descendants] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
- [:descendants, :inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct, :descendants, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [] | :group | []
+ GroupMembersFinder::DEFAULT_RELATIONS | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
+ [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
+ [:inherited] | :group | []
+ [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
+ [:shared_from_groups] | :group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
+ [:direct, :inherited, :descendants, :shared_from_groups] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
+ [] | :sub_group | []
+ GroupMembersFinder::DEFAULT_RELATIONS | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
+ [:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
+ [:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
+ [:shared_from_groups] | :sub_group | []
+ [:direct, :inherited, :descendants, :shared_from_groups] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [] | :sub_sub_group | []
+ GroupMembersFinder::DEFAULT_RELATIONS | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
+ [:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:descendants] | :sub_sub_group | []
+ [:shared_from_groups] | :sub_sub_group | []
+ [:direct, :inherited, :descendants, :shared_from_groups] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
end
with_them do
it 'returns correct members' do
- result = if subject_relations
- described_class.new(groups[subject_group]).execute(include_relations: subject_relations)
- else
- described_class.new(groups[subject_group]).execute
- end
+ result = described_class.new(groups[subject_group]).execute(include_relations: subject_relations)
expect(result.to_a).to match_array(expected_members.map { |name| members[name] })
end
diff --git a/spec/finders/groups/user_groups_finder_spec.rb b/spec/finders/groups/user_groups_finder_spec.rb
index 4cce3ab72eb..a4a9b8d16d0 100644
--- a/spec/finders/groups/user_groups_finder_spec.rb
+++ b/spec/finders/groups/user_groups_finder_spec.rb
@@ -59,23 +59,6 @@ RSpec.describe Groups::UserGroupsFinder do
)
end
- context 'when paginatable_namespace_drop_down_for_project_creation feature flag is disabled' do
- before do
- stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
- end
-
- it 'ignores project creation scope and returns all groups where the user is a direct member' do
- is_expected.to match(
- [
- public_maintainer_group,
- private_maintainer_group,
- public_developer_group,
- guest_group
- ]
- )
- end
- end
-
context 'when search is provided' do
let(:arguments) { { permission_scope: :create_projects, search: 'maintainer' } }
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 03639bc0b98..0b6c438fd02 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -278,33 +278,38 @@ RSpec.describe MergeRequestsFinder do
end
describe 'draft state' do
- let!(:wip_merge_request1) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') }
- let!(:wip_merge_request2) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') }
- let!(:wip_merge_request3) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') }
- let!(:wip_merge_request4) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') }
- let!(:draft_merge_request1) { create(:merge_request, :simple, author: user, source_branch: 'draft1', source_project: project5, target_project: project5, title: 'Draft: thing') }
- let!(:draft_merge_request2) { create(:merge_request, :simple, author: user, source_branch: 'draft2', source_project: project6, target_project: project6, title: '[draft] thing') }
- let!(:draft_merge_request3) { create(:merge_request, :simple, author: user, source_branch: 'draft3', source_project: project1, target_project: project1, title: '(draft) thing') }
- let!(:draft_merge_request4) { create(:merge_request, :simple, author: user, source_branch: 'draft4', source_project: project1, target_project: project2, title: 'Draft - thing') }
-
- [:wip, :draft].each do |draft_param_key|
- it "filters by #{draft_param_key}" do
- params = { draft_param_key => 'yes' }
+ shared_examples 'draft MRs filtering' do |draft_param_key, draft_param_value, title_prefix, draft_only|
+ it "filters by #{draft_param_key} => #{draft_param_value}" do
+ merge_request1.reload.update!(title: "#{title_prefix} #{merge_request1.title}")
+
+ params = { draft_param_key => draft_param_value }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(
- merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4,
- draft_merge_request1, draft_merge_request2, draft_merge_request3, draft_merge_request4
- )
+ if draft_only
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5)
+ else
+ expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
+ end
end
+ end
- it "filters by not #{draft_param_key}" do
- params = { draft_param_key => 'no' }
-
- merge_requests = described_class.new(user, params).execute
+ {
+ wip: ["WIP:", "wip", "[wip]"],
+ draft: ["Draft:", "Draft -", "[Draft]", "(Draft)"]
+ }.each do |draft_param_key, title_prefixes|
+ title_prefixes.each do |title_prefix|
+ it_behaves_like 'draft MRs filtering', draft_param_key, 1, title_prefix, true
+ it_behaves_like 'draft MRs filtering', draft_param_key, '1', title_prefix, true
+ it_behaves_like 'draft MRs filtering', draft_param_key, true, title_prefix, true
+ it_behaves_like 'draft MRs filtering', draft_param_key, 'true', title_prefix, true
+ it_behaves_like 'draft MRs filtering', draft_param_key, 'yes', title_prefix, true
- expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3)
+ it_behaves_like 'draft MRs filtering', draft_param_key, 0, title_prefix, false
+ it_behaves_like 'draft MRs filtering', draft_param_key, '0', title_prefix, false
+ it_behaves_like 'draft MRs filtering', draft_param_key, false, title_prefix, false
+ it_behaves_like 'draft MRs filtering', draft_param_key, 'false', title_prefix, false
+ it_behaves_like 'draft MRs filtering', draft_param_key, 'no', title_prefix, false
end
it "returns all items if no valid #{draft_param_key} param exists" do
@@ -313,43 +318,41 @@ RSpec.describe MergeRequestsFinder do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(
- merge_request1, merge_request2, merge_request3, merge_request4,
- merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4,
- draft_merge_request1, draft_merge_request2, draft_merge_request3, draft_merge_request4
+ merge_request1, merge_request2, merge_request3, merge_request4, merge_request5
)
end
end
+ end
- context 'filter by deployment' do
- let_it_be(:project_with_repo) { create(:project, :repository) }
+ context 'filter by deployment' do
+ let_it_be(:project_with_repo) { create(:project, :repository) }
- it 'returns the relevant merge requests' do
- deployment1 = create(
- :deployment,
- project: project_with_repo,
- sha: project_with_repo.commit.id
- )
- deployment2 = create(
- :deployment,
- project: project_with_repo,
- sha: project_with_repo.commit.id
- )
- deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
- deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id))
+ it 'returns the relevant merge requests' do
+ deployment1 = create(
+ :deployment,
+ project: project_with_repo,
+ sha: project_with_repo.commit.id
+ )
+ deployment2 = create(
+ :deployment,
+ project: project_with_repo,
+ sha: project_with_repo.commit.id
+ )
+ deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
+ deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id))
- params = { deployment_id: deployment1.id }
- merge_requests = described_class.new(user, params).execute
+ params = { deployment_id: deployment1.id }
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
- end
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
+ end
- context 'when a deployment does not contain any merge requests' do
- it 'returns an empty result' do
- params = { deployment_id: create(:deployment, project: project_with_repo, sha: project_with_repo.commit.sha).id }
- merge_requests = described_class.new(user, params).execute
+ context 'when a deployment does not contain any merge requests' do
+ it 'returns an empty result' do
+ params = { deployment_id: create(:deployment, project: project_with_repo, sha: project_with_repo.commit.sha).id }
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to be_empty
- end
+ expect(merge_requests).to be_empty
end
end
end
diff --git a/spec/finders/packages/conan/package_file_finder_spec.rb b/spec/finders/packages/conan/package_file_finder_spec.rb
index c2f445c58f7..3da7da456c2 100644
--- a/spec/finders/packages/conan/package_file_finder_spec.rb
+++ b/spec/finders/packages/conan/package_file_finder_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
let(:package_file_name) { package_file.file_name }
let(:params) { {} }
- RSpec.shared_examples 'package file finder examples' do
+ shared_examples 'package file finder examples' do
it { is_expected.to eq(package_file) }
context 'with conan_file_type' do
@@ -39,11 +39,37 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
end
end
+ shared_examples 'not returning pending_destruction package files' do
+ let_it_be(:recent_package_file_pending_destruction) do
+ create(:package_file, :pending_destruction, package: package, file_name: package_file.file_name)
+ end
+
+ it 'returns the correct package file' do
+ expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
+
+ expect(subject).to eq(package_file)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns the correct package file' do
+ expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
+
+ expect(subject).to eq(recent_package_file_pending_destruction)
+ end
+ end
+ end
+
describe '#execute' do
subject { described_class.new(package, package_file_name, params).execute }
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
@@ -56,6 +82,8 @@ RSpec.describe ::Packages::Conan::PackageFileFinder do
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
diff --git a/spec/finders/packages/go/package_finder_spec.rb b/spec/finders/packages/go/package_finder_spec.rb
index dbcb8255d47..b928336f958 100644
--- a/spec/finders/packages/go/package_finder_spec.rb
+++ b/spec/finders/packages/go/package_finder_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe Packages::Go::PackageFinder do
let(:version_name) { version.name }
before do
- package.update_column(:status, 1)
+ package.update_column(:status, :error)
end
it { is_expected.to eq(nil) }
diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb
index 38fc3b7cce4..8b45dbdad51 100644
--- a/spec/finders/packages/maven/package_finder_spec.rb
+++ b/spec/finders/packages/maven/package_finder_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do
let(:param_path) { package.maven_metadatum.path }
before do
- package.update_column(:status, 1)
+ package.update_column(:status, :error)
end
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb
index 230d267e508..7fabb3eed86 100644
--- a/spec/finders/packages/npm/package_finder_spec.rb
+++ b/spec/finders/packages/npm/package_finder_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe ::Packages::Npm::PackageFinder do
context 'with an uninstallable package' do
before do
- package.update_column(:status, 1)
+ package.update_column(:status, :error)
end
it { is_expected.to be_empty }
diff --git a/spec/finders/packages/nuget/package_finder_spec.rb b/spec/finders/packages/nuget/package_finder_spec.rb
index 045dba295ac..415bf796a72 100644
--- a/spec/finders/packages/nuget/package_finder_spec.rb
+++ b/spec/finders/packages/nuget/package_finder_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Packages::Nuget::PackageFinder do
context 'with an uninstallable package' do
before do
- package1.update_column(:status, 1)
+ package1.update_column(:status, :error)
end
it { is_expected.to contain_exactly(package2) }
diff --git a/spec/finders/packages/package_file_finder_spec.rb b/spec/finders/packages/package_file_finder_spec.rb
index 8014f04d917..8b21c9cd3ec 100644
--- a/spec/finders/packages/package_file_finder_spec.rb
+++ b/spec/finders/packages/package_file_finder_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Packages::PackageFileFinder do
let(:package_file_name) { package_file.file_name }
let(:params) { {} }
- RSpec.shared_examples 'package file finder examples' do
+ shared_examples 'package file finder examples' do
it { is_expected.to eq(package_file) }
context 'with file_name_like' do
@@ -19,11 +19,35 @@ RSpec.describe Packages::PackageFileFinder do
end
end
+ shared_examples 'not returning pending_destruction package files' do
+ let_it_be(:recent_package_file_pending_destruction) do
+ create(:package_file, :pending_destruction, package: package, file_name: package_file.file_name)
+ end
+
+ it 'returns the correct package file' do
+ expect(package.package_files.last).to eq(recent_package_file_pending_destruction)
+
+ expect(subject).to eq(package_file)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(subject).to eq(recent_package_file_pending_destruction)
+ end
+ end
+ end
+
describe '#execute' do
subject { described_class.new(package, package_file_name, params).execute }
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
@@ -36,6 +60,8 @@ RSpec.describe Packages::PackageFileFinder do
it_behaves_like 'package file finder examples'
+ it_behaves_like 'not returning pending_destruction package files'
+
context 'with unknown file_name' do
let(:package_file_name) { 'unknown.jpg' }
diff --git a/spec/finders/user_group_notification_settings_finder_spec.rb b/spec/finders/user_group_notification_settings_finder_spec.rb
index ea44688bc8d..ac59a42d813 100644
--- a/spec/finders/user_group_notification_settings_finder_spec.rb
+++ b/spec/finders/user_group_notification_settings_finder_spec.rb
@@ -11,167 +11,155 @@ RSpec.describe UserGroupNotificationSettingsFinder do
subject.map(&proc).uniq
end
- shared_examples 'user group notifications settings tests' do
- context 'when the groups have no existing notification settings' do
- context 'when the groups have no ancestors' do
- let_it_be(:groups) { create_list(:group, 3) }
-
- it 'will be a default Global notification setting', :aggregate_failures do
- expect(subject.count).to eq(3)
- expect(attributes(&:notification_email)).to match_array([nil])
- expect(attributes(&:level)).to match_array(['global'])
- end
+ context 'when the groups have no existing notification settings' do
+ context 'when the groups have no ancestors' do
+ let_it_be(:groups) { create_list(:group, 3) }
+
+ it 'will be a default Global notification setting', :aggregate_failures do
+ expect(subject.count).to eq(3)
+ expect(attributes(&:notification_email)).to match_array([nil])
+ expect(attributes(&:level)).to match_array(['global'])
end
+ end
- context 'when the groups have ancestors' do
- context 'when an ancestor has a level other than Global' do
- let_it_be(:ancestor_a) { create(:group) }
- let_it_be(:group_a) { create(:group, parent: ancestor_a) }
- let_it_be(:ancestor_b) { create(:group) }
- let_it_be(:group_b) { create(:group, parent: ancestor_b) }
- let_it_be(:email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
-
- let_it_be(:groups) { [group_a, group_b] }
+ context 'when the groups have ancestors' do
+ context 'when an ancestor has a level other than Global' do
+ let_it_be(:ancestor_a) { create(:group) }
+ let_it_be(:group_a) { create(:group, parent: ancestor_a) }
+ let_it_be(:ancestor_b) { create(:group) }
+ let_it_be(:group_b) { create(:group, parent: ancestor_b) }
+ let_it_be(:email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
- before do
- create(:notification_setting, user: user, source: ancestor_a, level: 'participating', notification_email: email.email)
- create(:notification_setting, user: user, source: ancestor_b, level: 'participating', notification_email: email.email)
- end
+ let_it_be(:groups) { [group_a, group_b] }
- it 'has the same level set' do
- expect(attributes(&:level)).to match_array(['participating'])
- end
+ before do
+ create(:notification_setting, user: user, source: ancestor_a, level: 'participating', notification_email: email.email)
+ create(:notification_setting, user: user, source: ancestor_b, level: 'participating', notification_email: email.email)
+ end
- it 'has the same email set' do
- expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
- end
+ it 'has the same level set' do
+ expect(attributes(&:level)).to match_array(['participating'])
+ end
- it 'only returns the two queried groups' do
- expect(subject.count).to eq(2)
- end
+ it 'has the same email set' do
+ expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
end
- context 'when an ancestor has a Global level but has an email set' do
- let_it_be(:grand_ancestor) { create(:group) }
- let_it_be(:ancestor) { create(:group, parent: grand_ancestor) }
- let_it_be(:group) { create(:group, parent: ancestor) }
- let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
- let_it_be(:grand_email) { create(:email, :confirmed, email: 'grand@example.com', user: user) }
-
- let_it_be(:groups) { [group] }
-
- before do
- create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: grand_email.email)
- create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: ancestor_email.email)
- end
-
- it 'has the same email and level set', :aggregate_failures do
- expect(subject.count).to eq(1)
- expect(attributes(&:level)).to match_array(['global'])
- expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
- end
+ it 'only returns the two queried groups' do
+ expect(subject.count).to eq(2)
end
+ end
- context 'when the group has parent_id set but that does not belong to any group' do
- let_it_be(:group) { create(:group) }
- let_it_be(:groups) { [group] }
+ context 'when an ancestor has a Global level but has an email set' do
+ let_it_be(:grand_ancestor) { create(:group) }
+ let_it_be(:ancestor) { create(:group, parent: grand_ancestor) }
+ let_it_be(:group) { create(:group, parent: ancestor) }
+ let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
+ let_it_be(:grand_email) { create(:email, :confirmed, email: 'grand@example.com', user: user) }
- before do
- # Let's set a parent_id for a group that definitely doesn't exist
- group.update_columns(parent_id: 19283746)
- end
+ let_it_be(:groups) { [group] }
- it 'returns a default Global notification setting' do
- expect(subject.count).to eq(1)
- expect(attributes(&:level)).to match_array(['global'])
- expect(attributes(&:notification_email)).to match_array([nil])
- end
+ before do
+ create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: grand_email.email)
+ create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: ancestor_email.email)
end
- context 'when the group has a private parent' do
- let_it_be(:ancestor) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: ancestor) }
- let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
- let_it_be(:groups) { [group] }
-
- before do
- group.add_reporter(user)
- # Adding the user creates a NotificationSetting, so we remove it here
- user.notification_settings.where(source: group).delete_all
-
- create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: ancestor_email.email)
- end
-
- it 'still inherits the notification settings' do
- expect(subject.count).to eq(1)
- expect(attributes(&:level)).to match_array(['participating'])
- expect(attributes(&:notification_email)).to match_array([ancestor_email.email])
- end
+ it 'has the same email and level set', :aggregate_failures do
+ expect(subject.count).to eq(1)
+ expect(attributes(&:level)).to match_array(['global'])
+ expect(attributes(&:notification_email)).to match_array(['ancestor@example.com'])
end
+ end
- it 'does not cause an N+1', :aggregate_failures do
- parent = create(:group)
- child = create(:group, parent: parent)
-
- control = ActiveRecord::QueryRecorder.new do
- described_class.new(user, Group.where(id: child.id)).execute
- end
-
- other_parent = create(:group)
- other_children = create_list(:group, 2, parent: other_parent)
-
- result = nil
+ context 'when the group has parent_id set but that does not belong to any group' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:groups) { [group] }
- expect do
- result = described_class.new(user, Group.where(id: other_children.append(child).map(&:id))).execute
- end.not_to exceed_query_limit(control)
+ before do
+ # Let's set a parent_id for a group that definitely doesn't exist
+ group.update_columns(parent_id: 19283746)
+ end
- expect(result.count).to eq(3)
+ it 'returns a default Global notification setting' do
+ expect(subject.count).to eq(1)
+ expect(attributes(&:level)).to match_array(['global'])
+ expect(attributes(&:notification_email)).to match_array([nil])
end
end
- end
- context 'preloading `emails_disabled`' do
- let_it_be(:root_group) { create(:group) }
- let_it_be(:sub_group) { create(:group, parent: root_group) }
- let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
+ context 'when the group has a private parent' do
+ let_it_be(:ancestor) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: ancestor) }
+ let_it_be(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
+ let_it_be(:groups) { [group] }
- let_it_be(:another_root_group) { create(:group) }
- let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_disabled: true, parent: another_root_group) }
- let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
+ before do
+ group.add_reporter(user)
+ # Adding the user creates a NotificationSetting, so we remove it here
+ user.notification_settings.where(source: group).delete_all
- let_it_be(:root_group_with_emails_disabled) { create(:group, emails_disabled: true) }
- let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
-
- let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
+ create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: ancestor_email.email)
+ end
- before do
- described_class.new(user, groups).execute
+ it 'still inherits the notification settings' do
+ expect(subject.count).to eq(1)
+ expect(attributes(&:level)).to match_array(['participating'])
+ expect(attributes(&:notification_email)).to match_array([ancestor_email.email])
+ end
end
- it 'preloads the `group.emails_disabled` method' do
- recorder = ActiveRecord::QueryRecorder.new do
- groups.each(&:emails_disabled?)
+ it 'does not cause an N+1', :aggregate_failures do
+ parent = create(:group)
+ child = create(:group, parent: parent)
+
+ control = ActiveRecord::QueryRecorder.new do
+ described_class.new(user, Group.where(id: child.id)).execute
end
- expect(recorder.count).to eq(0)
- end
+ other_parent = create(:group)
+ other_children = create_list(:group, 2, parent: other_parent)
- it 'preloads the `group.emails_disabled` method correctly' do
- groups.each do |group|
- expect(group.emails_disabled?).to eq(Group.find(group.id).emails_disabled?) # compare the memoized and the freshly loaded value
- end
+ result = nil
+
+ expect do
+ result = described_class.new(user, Group.where(id: other_children.append(child).map(&:id))).execute
+ end.not_to exceed_query_limit(control)
+
+ expect(result.count).to eq(3)
end
end
end
- it_behaves_like 'user group notifications settings tests'
+ context 'preloading `emails_disabled`' do
+ let_it_be(:root_group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: root_group) }
+ let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
+
+ let_it_be(:another_root_group) { create(:group) }
+ let_it_be(:sub_group_with_emails_disabled) { create(:group, emails_disabled: true, parent: another_root_group) }
+ let_it_be(:another_sub_sub_group) { create(:group, parent: sub_group_with_emails_disabled) }
+
+ let_it_be(:root_group_with_emails_disabled) { create(:group, emails_disabled: true) }
+ let_it_be(:group) { create(:group, parent: root_group_with_emails_disabled) }
+
+ let(:groups) { Group.where(id: [sub_sub_group, another_sub_sub_group, group]) }
- context 'when feature flag :linear_user_group_notification_settings_finder_ancestors_scopes is disabled' do
before do
- stub_feature_flags(linear_user_group_notification_settings_finder_ancestors_scopes: false)
+ described_class.new(user, groups).execute
+ end
+
+ it 'preloads the `group.emails_disabled` method' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ groups.each(&:emails_disabled?)
+ end
+
+ expect(recorder.count).to eq(0)
end
- it_behaves_like 'user group notifications settings tests'
+ it 'preloads the `group.emails_disabled` method correctly' do
+ groups.each do |group|
+ expect(group.emails_disabled?).to eq(Group.find(group.id).emails_disabled?) # compare the memoized and the freshly loaded value
+ 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 74c563b9bf6..6019d22059d 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -59,14 +59,46 @@ RSpec.describe UserRecentEventsFinder do
expect(events.size).to eq(6)
end
+ context 'selected events' do
+ let!(:push_event) { create(:push_event, project: public_project, author: project_owner) }
+ let!(:push_event_second_user) { create(:push_event, project: public_project_second_user, author: second_user) }
+
+ it 'only includes selected events (PUSH) from all users', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::PUSH)
+ events = described_class.new(current_user, [project_owner, second_user], event_filter, params).execute
+
+ expect(events).to contain_exactly(push_event, push_event_second_user)
+ end
+ end
+
it 'does not include events from users with private profile', :aggregate_failures do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, second_user).and_return(false)
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
- expect(events).to include(private_event, internal_event, public_event)
- expect(events.size).to eq(3)
+ expect(events).to contain_exactly(private_event, internal_event, public_event)
+ end
+
+ context 'with pagination params' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:limit, :offset, :ordered_expected_events) do
+ nil | nil | lazy { [public_event_second_user, internal_event_second_user, private_event_second_user, public_event, internal_event, private_event] }
+ 2 | nil | lazy { [public_event_second_user, internal_event_second_user] }
+ nil | 4 | lazy { [internal_event, private_event] }
+ 2 | 2 | lazy { [private_event_second_user, public_event] }
+ end
+
+ with_them do
+ let(:params) { { limit: limit, offset: offset }.compact }
+
+ it 'returns paginated events sorted by id (DESC)' do
+ events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
+
+ expect(events).to eq(ordered_expected_events)
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 9ef7f6c9271..50e52a7bb87 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -149,6 +149,30 @@
}
}
}
+ },
+ "npmUrl": {
+ "type": "string"
+ },
+ "mavenUrl": {
+ "type": "string"
+ },
+ "conanUrl": {
+ "type": "string"
+ },
+ "nugetUrl": {
+ "type": "string"
+ },
+ "pypiUrl": {
+ "type": "string"
+ },
+ "pypiSetupUrl": {
+ "type": "string"
+ },
+ "composerUrl": {
+ "type": "string"
+ },
+ "composerConfigRepositoryUrl": {
+ "type": "string"
}
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
index c31e91cfef8..a55c4b8974b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -20,6 +20,18 @@
},
"additionalProperties": false
},
+ "merge_user": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
"merged_at": { "type": ["string", "null"] },
"closed_by": {
"type": ["object", "null"],
diff --git a/spec/fixtures/ci_secure_files/upload-keystore.jks b/spec/fixtures/ci_secure_files/upload-keystore.jks
new file mode 100644
index 00000000000..715adad4a89
--- /dev/null
+++ b/spec/fixtures/ci_secure_files/upload-keystore.jks
Binary files differ
diff --git a/spec/fixtures/error_tracking/go_two_exception_event.json b/spec/fixtures/error_tracking/go_two_exception_event.json
new file mode 100644
index 00000000000..97ed8372a27
--- /dev/null
+++ b/spec/fixtures/error_tracking/go_two_exception_event.json
@@ -0,0 +1 @@
+{"contexts":{"device":{"arch":"amd64","num_cpu":16},"os":{"name":"darwin"},"runtime":{"go_maxprocs":16,"go_numcgocalls":1,"go_numroutines":2,"name":"go","version":"go1.16.10"}},"event_id":"f92492349cda4ceaba1aab9dac55a412","level":"error","platform":"go","release":"v0.12.0-1-g6b72962","sdk":{"name":"sentry.go","version":"0.12.0","integrations":["ContextifyFrames","Environment","IgnoreErrors","Modules"],"packages":[{"name":"sentry-go","version":"0.12.0"}]},"server_name":"jet.fios-router.home","user":{},"modules":{"github.com/getsentry/sentry-go":"(devel)","golang.org/x/sys":"v0.0.0-20211007075335-d3039528d8ac"},"exception":[{"type":"*errors.errorString","value":"unsupported protocol scheme \"\""},{"type":"*url.Error","value":"Get \"foobar\": unsupported protocol scheme \"\"","stacktrace":{"frames":[{"function":"main","module":"main","abs_path":"/Users/stanhu/github/sentry-go/example/basic/main.go","lineno":54,"pre_context":["\t// Set the timeout to the maximum duration the program can afford to wait.","\tdefer sentry.Flush(2 * time.Second)","","\tresp, err := http.Get(os.Args[1])","\tif err != nil {"],"context_line":"\t\tsentry.CaptureException(err)","post_context":["\t\tlog.Printf(\"reported to Sentry: %s\", err)","\t\treturn","\t}","\tdefer resp.Body.Close()",""],"in_app":true}]}}],"timestamp":"2021-12-25T22:32:06.191665-08:00"}
diff --git a/spec/fixtures/security_reports/master/gl-sast-report.json b/spec/fixtures/security_reports/master/gl-sast-report.json
index 3323c1fffe3..63504e6fccc 100644
--- a/spec/fixtures/security_reports/master/gl-sast-report.json
+++ b/spec/fixtures/security_reports/master/gl-sast-report.json
@@ -26,6 +26,16 @@
"value": "PREDICTABLE_RANDOM",
"url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
}
+ ],
+ "links": [
+ {
+ "name": "Link1",
+ "url": "https://www.url1.com"
+ },
+ {
+ "name": "Link2",
+ "url": "https://www.url2.com"
+ }
]
},
{
diff --git a/spec/frontend/__helpers__/matchers.js b/spec/frontend/__helpers__/matchers.js
deleted file mode 100644
index 945abdafe9a..00000000000
--- a/spec/frontend/__helpers__/matchers.js
+++ /dev/null
@@ -1,68 +0,0 @@
-export default {
- toHaveSpriteIcon: (element, iconName) => {
- if (!iconName) {
- throw new Error('toHaveSpriteIcon is missing iconName argument!');
- }
-
- if (!(element instanceof HTMLElement)) {
- throw new Error(`${element} is not a DOM element!`);
- }
-
- const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
- const matchingIcon = iconReferences.find(
- (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
- );
-
- const pass = Boolean(matchingIcon);
-
- let message;
- if (pass) {
- message = `${element.outerHTML} contains the sprite icon "${iconName}"!`;
- } else {
- message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
-
- const existingIcons = iconReferences.map((reference) => {
- const iconUrl = reference.getAttribute('href');
- return `"${iconUrl.replace(/^.+#/, '')}"`;
- });
- if (existingIcons.length > 0) {
- message += ` (only found ${existingIcons.join(',')})`;
- }
- }
-
- return {
- pass,
- message: () => message,
- };
- },
- toMatchInterpolatedText(received, match) {
- let clearReceived;
- let clearMatch;
-
- try {
- clearReceived = received.replace(/\s\s+/gm, ' ').replace(/\s\./gm, '.').trim();
- } catch (e) {
- return { actual: received, message: 'The received value is not a string', pass: false };
- }
- try {
- clearMatch = match.replace(/%{\w+}/gm, '').trim();
- } catch (e) {
- return { message: 'The comparator value is not a string', pass: false };
- }
- const pass = clearReceived === clearMatch;
- const message = pass
- ? () => `
- \n\n
- Expected: ${this.utils.printExpected(clearReceived)}
- To not equal: ${this.utils.printReceived(clearMatch)}
- `
- : () =>
- `
- \n\n
- Expected: ${this.utils.printExpected(clearReceived)}
- To equal: ${this.utils.printReceived(clearMatch)}
- `;
-
- return { actual: received, message, pass };
- },
-};
diff --git a/spec/frontend/__helpers__/matchers/index.js b/spec/frontend/__helpers__/matchers/index.js
new file mode 100644
index 00000000000..76571bafb06
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/index.js
@@ -0,0 +1,3 @@
+export * from './to_have_sprite_icon';
+export * from './to_have_tracking_attributes';
+export * from './to_match_interpolated_text';
diff --git a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
new file mode 100644
index 00000000000..bce9d93bea8
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
@@ -0,0 +1,36 @@
+export const toHaveSpriteIcon = (element, iconName) => {
+ if (!iconName) {
+ throw new Error('toHaveSpriteIcon is missing iconName argument!');
+ }
+
+ if (!(element instanceof HTMLElement)) {
+ throw new Error(`${element} is not a DOM element!`);
+ }
+
+ const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
+ const matchingIcon = iconReferences.find(
+ (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
+ );
+
+ const pass = Boolean(matchingIcon);
+
+ let message;
+ if (pass) {
+ message = `${element.outerHTML} contains the sprite icon "${iconName}"!`;
+ } else {
+ message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
+
+ const existingIcons = iconReferences.map((reference) => {
+ const iconUrl = reference.getAttribute('href');
+ return `"${iconUrl.replace(/^.+#/, '')}"`;
+ });
+ if (existingIcons.length > 0) {
+ message += ` (only found ${existingIcons.join(',')})`;
+ }
+ }
+
+ return {
+ pass,
+ message: () => message,
+ };
+};
diff --git a/spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js
new file mode 100644
index 00000000000..fd3f3aa042f
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js
@@ -0,0 +1,35 @@
+import { diff } from 'jest-diff';
+import { isObject, mapValues, isEqual } from 'lodash';
+
+export const toHaveTrackingAttributes = (actual, obj) => {
+ if (!(actual instanceof Element)) {
+ return { actual, message: () => 'The received value must be an Element.', pass: false };
+ }
+
+ if (!isObject(obj)) {
+ return {
+ message: () => `The matching object must be an object. Found ${obj}.`,
+ pass: false,
+ };
+ }
+
+ const actualAttributes = mapValues(obj, (val, key) => actual.getAttribute(`data-track-${key}`));
+
+ const matcherPass = isEqual(actualAttributes, obj);
+
+ const failMessage = () => {
+ // We can match, but still fail because we're in a `expect...not.` context
+ if (matcherPass) {
+ return `Expected the element's tracking attributes not to match. Found that they matched ${JSON.stringify(
+ obj,
+ )}.`;
+ }
+
+ const objDiff = diff(actualAttributes, obj);
+ return `Expected the element's tracking attributes to match the given object. Diff:
+${objDiff}
+`;
+ };
+
+ return { actual, message: failMessage, pass: matcherPass };
+};
diff --git a/spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js
new file mode 100644
index 00000000000..74073ed4063
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js
@@ -0,0 +1,65 @@
+import { diff } from 'jest-diff';
+
+describe('custom matcher toHaveTrackingAttributes', () => {
+ const createElementWithAttrs = (attributes) => {
+ const el = document.createElement('div');
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ el.setAttribute(key, value);
+ });
+
+ return el;
+ };
+
+ it('blows up if actual is not an element', () => {
+ expect(() => {
+ expect({}).toHaveTrackingAttributes({});
+ }).toThrow('The received value must be an Element.');
+ });
+
+ it('blows up if expected is not an object', () => {
+ expect(() => {
+ expect(createElementWithAttrs({})).toHaveTrackingAttributes('foo');
+ }).toThrow('The matching object must be an object.');
+ });
+
+ it('prints diff when fails', () => {
+ const expectedDiff = diff({ label: 'foo' }, { label: 'a' });
+ expect(() => {
+ expect(createElementWithAttrs({ 'data-track-label': 'foo' })).toHaveTrackingAttributes({
+ label: 'a',
+ });
+ }).toThrow(
+ `Expected the element's tracking attributes to match the given object. Diff:\n${expectedDiff}\n`,
+ );
+ });
+
+ describe('positive assertions', () => {
+ it.each`
+ attrs | expected
+ ${{ 'data-track-label': 'foo' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'foo' }} | ${{}}
+ ${{ 'data-track-label': 'foo', label: 'bar' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ label: 'foo', extra: '123' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ extra: '123' }}
+ ${{ label: 'foo', extra: '123', id: '7' }} | ${{}}
+ `('$expected matches element with attrs $attrs', ({ attrs, expected }) => {
+ expect(createElementWithAttrs(attrs)).toHaveTrackingAttributes(expected);
+ });
+ });
+
+ describe('negative assertions', () => {
+ it.each`
+ attrs | expected
+ ${{}} | ${{ label: 'foo' }}
+ ${{ label: 'foo' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'bar', label: 'foo' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'foo' }} | ${{ extra: '123' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ label: 'foo', extra: '456' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ label: 'foo', extra: '123', action: 'click' }}
+ ${{ label: 'foo', extra: '123', id: '7' }} | ${{ id: '7' }}
+ `('$expected does not match element with attrs $attrs', ({ attrs, expected }) => {
+ expect(createElementWithAttrs(attrs)).not.toHaveTrackingAttributes(expected);
+ });
+ });
+});
diff --git a/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js b/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
new file mode 100644
index 00000000000..4ce814a01b4
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
@@ -0,0 +1,30 @@
+export const toMatchInterpolatedText = (received, match) => {
+ let clearReceived;
+ let clearMatch;
+
+ try {
+ clearReceived = received.replace(/\s\s+/gm, ' ').replace(/\s\./gm, '.').trim();
+ } catch (e) {
+ return { actual: received, message: 'The received value is not a string', pass: false };
+ }
+ try {
+ clearMatch = match.replace(/%{\w+}/gm, '').trim();
+ } catch (e) {
+ return { message: 'The comparator value is not a string', pass: false };
+ }
+ const pass = clearReceived === clearMatch;
+ const message = pass
+ ? () => `
+ \n\n
+ Expected: ${this.utils.printExpected(clearReceived)}
+ To not equal: ${this.utils.printReceived(clearMatch)}
+ `
+ : () =>
+ `
+ \n\n
+ Expected: ${this.utils.printExpected(clearReceived)}
+ To equal: ${this.utils.printReceived(clearMatch)}
+ `;
+
+ return { actual: received, message, pass };
+};
diff --git a/spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js b/spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js
new file mode 100644
index 00000000000..f6fd00011fe
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js
@@ -0,0 +1,46 @@
+describe('custom matcher toMatchInterpolatedText', () => {
+ describe('malformed input', () => {
+ it.each([null, 1, Symbol, Array, Object])(
+ 'fails graciously if the expected value is %s',
+ (expected) => {
+ expect(expected).not.toMatchInterpolatedText('null');
+ },
+ );
+ });
+ describe('malformed matcher', () => {
+ it.each([null, 1, Symbol, Array, Object])(
+ 'fails graciously if the matcher is %s',
+ (matcher) => {
+ expect('null').not.toMatchInterpolatedText(matcher);
+ },
+ );
+ });
+
+ describe('positive assertion', () => {
+ it.each`
+ htmlString | templateString
+ ${'foo'} | ${'foo'}
+ ${'foo'} | ${'foo%{foo}'}
+ ${'foo '} | ${'foo'}
+ ${'foo '} | ${'foo%{foo}'}
+ ${'foo . '} | ${'foo%{foo}.'}
+ ${'foo bar . '} | ${'foo%{foo} bar.'}
+ ${'foo\n\nbar . '} | ${'foo%{foo} bar.'}
+ ${'foo bar . .'} | ${'foo%{fooStart} bar.%{fooEnd}.'}
+ `('$htmlString equals $templateString', ({ htmlString, templateString }) => {
+ expect(htmlString).toMatchInterpolatedText(templateString);
+ });
+ });
+
+ describe('negative assertion', () => {
+ it.each`
+ htmlString | templateString
+ ${'foo'} | ${'bar'}
+ ${'foo'} | ${'bar%{foo}'}
+ ${'foo'} | ${'@{lol}foo%{foo}'}
+ ${' fo o '} | ${'foo'}
+ `('$htmlString does not equal $templateString', ({ htmlString, templateString }) => {
+ expect(htmlString).not.toMatchInterpolatedText(templateString);
+ });
+ });
+});
diff --git a/spec/frontend/__helpers__/matchers_spec.js b/spec/frontend/__helpers__/matchers_spec.js
deleted file mode 100644
index dfd6f754c72..00000000000
--- a/spec/frontend/__helpers__/matchers_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-describe('Custom jest matchers', () => {
- describe('toMatchInterpolatedText', () => {
- describe('malformed input', () => {
- it.each([null, 1, Symbol, Array, Object])(
- 'fails graciously if the expected value is %s',
- (expected) => {
- expect(expected).not.toMatchInterpolatedText('null');
- },
- );
- });
- describe('malformed matcher', () => {
- it.each([null, 1, Symbol, Array, Object])(
- 'fails graciously if the matcher is %s',
- (matcher) => {
- expect('null').not.toMatchInterpolatedText(matcher);
- },
- );
- });
-
- describe('positive assertion', () => {
- it.each`
- htmlString | templateString
- ${'foo'} | ${'foo'}
- ${'foo'} | ${'foo%{foo}'}
- ${'foo '} | ${'foo'}
- ${'foo '} | ${'foo%{foo}'}
- ${'foo . '} | ${'foo%{foo}.'}
- ${'foo bar . '} | ${'foo%{foo} bar.'}
- ${'foo\n\nbar . '} | ${'foo%{foo} bar.'}
- ${'foo bar . .'} | ${'foo%{fooStart} bar.%{fooEnd}.'}
- `('$htmlString equals $templateString', ({ htmlString, templateString }) => {
- expect(htmlString).toMatchInterpolatedText(templateString);
- });
- });
-
- describe('negative assertion', () => {
- it.each`
- htmlString | templateString
- ${'foo'} | ${'bar'}
- ${'foo'} | ${'bar%{foo}'}
- ${'foo'} | ${'@{lol}foo%{foo}'}
- ${' fo o '} | ${'foo'}
- `('$htmlString does not equal $templateString', ({ htmlString, templateString }) => {
- expect(htmlString).not.toMatchInterpolatedText(templateString);
- });
- });
- });
-});
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 03389e16b65..7b5df18ee0f 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -8,7 +8,7 @@ import setWindowLocation from './set_window_location_helper';
import { setGlobalDateToFakeDate } from './fake_date';
import { loadHTMLFixture, setHTMLFixture } from './fixtures';
import { TEST_HOST } from './test_constants';
-import customMatchers from './matchers';
+import * as customMatchers from './matchers';
import './dom_shims';
import './jquery';
diff --git a/spec/frontend/__helpers__/wait_using_real_timer.js b/spec/frontend/__helpers__/wait_using_real_timer.js
deleted file mode 100644
index 110d5f46c08..00000000000
--- a/spec/frontend/__helpers__/wait_using_real_timer.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* useful for timing promises when jest fakeTimers are not reliable enough */
-export default (timeout) =>
- new Promise((resolve) => {
- jest.useRealTimers();
- setTimeout(resolve, timeout);
- jest.useFakeTimers();
- });
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index bdc1dde7d48..018303fcae7 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -319,6 +319,8 @@ describe('AlertsSettingsForm', () => {
const validPayloadMsg = payload === emptySamplePayload ? 'not valid' : 'valid';
it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and payload is ${validPayloadMsg}`, async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: { payloadExample: payload },
resetPayloadAndMappingConfirmed,
@@ -345,6 +347,8 @@ describe('AlertsSettingsForm', () => {
: 'was not confirmed';
it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: {
payloadExample,
@@ -359,6 +363,8 @@ describe('AlertsSettingsForm', () => {
describe('Parsing payload', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
resetPayloadAndMappingConfirmed: true,
});
@@ -456,6 +462,8 @@ describe('AlertsSettingsForm', () => {
});
it('should be able to submit when form is dirty', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: { type: typeSet.http, name: 'Existing integration' },
});
@@ -466,6 +474,8 @@ describe('AlertsSettingsForm', () => {
});
it('should not be able to submit when form is pristine', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: { type: typeSet.http, name: 'Existing integration' },
});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 5d681c7da4f..28d7ebe28df 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -126,6 +126,8 @@ describe('ProjectsDropdownFilter component', () => {
});
it('applies the correct queryParams when making an api call', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchTerm: 'gitlab' });
expect(spyQuery).toHaveBeenCalledTimes(1);
@@ -204,6 +206,8 @@ describe('ProjectsDropdownFilter component', () => {
await createWithMockDropdown({ multiSelect: true });
selectDropdownItemAtIndex(0);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchTerm: 'this is a very long search string' });
});
diff --git a/spec/frontend/api/packages_api_spec.js b/spec/frontend/api/packages_api_spec.js
index 3286dccb1b2..d55d2036dcf 100644
--- a/spec/frontend/api/packages_api_spec.js
+++ b/spec/frontend/api/packages_api_spec.js
@@ -38,12 +38,17 @@ describe('Api', () => {
mock.onPut(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
return publishPackage(
- { projectPath, name, version: 0, fileName: name, files: [{}] },
+ {
+ projectPath,
+ name,
+ version: 0,
+ fileName: name,
+ files: [new File(['zip contents'], 'bar.zip')],
+ },
{ status: 'hidden', select: 'package_file' },
).then(({ data }) => {
expect(data).toEqual(apiResponse);
- expect(axios.put).toHaveBeenCalledWith(expectedUrl, expect.any(FormData), {
- headers: { 'Content-Type': 'multipart/form-data' },
+ expect(axios.put).toHaveBeenCalledWith(expectedUrl, expect.any(File), {
params: { select: 'package_file', status: 'hidden' },
});
});
diff --git a/spec/frontend/behaviors/copy_to_clipboard_spec.js b/spec/frontend/behaviors/copy_to_clipboard_spec.js
new file mode 100644
index 00000000000..c5beaa0ba5d
--- /dev/null
+++ b/spec/frontend/behaviors/copy_to_clipboard_spec.js
@@ -0,0 +1,187 @@
+import initCopyToClipboard, {
+ CLIPBOARD_SUCCESS_EVENT,
+ CLIPBOARD_ERROR_EVENT,
+ I18N_ERROR_MESSAGE,
+} from '~/behaviors/copy_to_clipboard';
+import { show, hide, fixTitle, once } from '~/tooltips';
+
+let onceCallback = () => {};
+jest.mock('~/tooltips', () => ({
+ show: jest.fn(),
+ hide: jest.fn(),
+ fixTitle: jest.fn(),
+ once: jest.fn((event, callback) => {
+ onceCallback = callback;
+ }),
+}));
+
+describe('initCopyToClipboard', () => {
+ let clearSelection;
+ let focusSpy;
+ let dispatchEventSpy;
+ let button;
+ let clipboardInstance;
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ clipboardInstance = null;
+ });
+
+ const title = 'Copy this value';
+ const defaultButtonAttributes = {
+ 'data-clipboard-text': 'foo bar',
+ title,
+ 'data-title': title,
+ };
+ const createButton = (attributes = {}) => {
+ const combinedAttributes = { ...defaultButtonAttributes, ...attributes };
+ button = document.createElement('button');
+ Object.keys(combinedAttributes).forEach((attributeName) => {
+ button.setAttribute(attributeName, combinedAttributes[attributeName]);
+ });
+ document.body.appendChild(button);
+ };
+
+ const init = () => {
+ clipboardInstance = initCopyToClipboard();
+ };
+
+ const setupSpies = () => {
+ clearSelection = jest.fn();
+ focusSpy = jest.spyOn(button, 'focus');
+ dispatchEventSpy = jest.spyOn(button, 'dispatchEvent');
+ };
+
+ const emitSuccessEvent = () => {
+ clipboardInstance.emit('success', {
+ action: 'copy',
+ text: 'foo bar',
+ trigger: button,
+ clearSelection,
+ });
+ };
+
+ const emitErrorEvent = () => {
+ clipboardInstance.emit('error', {
+ action: 'copy',
+ text: 'foo bar',
+ trigger: button,
+ clearSelection,
+ });
+ };
+
+ const itHandlesTooltip = (expectedTooltip) => {
+ it('handles tooltip', () => {
+ expect(button.getAttribute('title')).toBe(expectedTooltip);
+ expect(button.getAttribute('aria-label')).toBe(expectedTooltip);
+ expect(fixTitle).toHaveBeenCalledWith(button);
+ expect(show).toHaveBeenCalledWith(button);
+ expect(once).toHaveBeenCalledWith('hidden', expect.any(Function));
+
+ expect(hide).not.toHaveBeenCalled();
+ jest.runAllTimers();
+ expect(hide).toHaveBeenCalled();
+
+ onceCallback({ target: button });
+ expect(button.getAttribute('title')).toBe(title);
+ expect(button.getAttribute('aria-label')).toBe(title);
+ expect(fixTitle).toHaveBeenCalledWith(button);
+ });
+ };
+
+ describe('when value is successfully copied', () => {
+ it(`calls clearSelection, focuses the button, and dispatches ${CLIPBOARD_SUCCESS_EVENT} event`, () => {
+ createButton();
+ init();
+ setupSpies();
+ emitSuccessEvent();
+
+ expect(clearSelection).toHaveBeenCalled();
+ expect(focusSpy).toHaveBeenCalled();
+ expect(dispatchEventSpy).toHaveBeenCalledWith(new Event(CLIPBOARD_SUCCESS_EVENT));
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `false`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'false',
+ });
+ init();
+ emitSuccessEvent();
+ });
+
+ it('does not handle success tooltip', () => {
+ expect(show).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `true`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'true',
+ });
+ init();
+ emitSuccessEvent();
+ });
+
+ itHandlesTooltip('Copied');
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is not set', () => {
+ beforeEach(() => {
+ createButton();
+ init();
+ emitSuccessEvent();
+ });
+
+ itHandlesTooltip('Copied');
+ });
+ });
+
+ describe('when there is an error copying the value', () => {
+ it(`dispatches ${CLIPBOARD_ERROR_EVENT} event`, () => {
+ createButton();
+ init();
+ setupSpies();
+ emitErrorEvent();
+
+ expect(dispatchEventSpy).toHaveBeenCalledWith(new Event(CLIPBOARD_ERROR_EVENT));
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `false`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'false',
+ });
+ init();
+ emitErrorEvent();
+ });
+
+ it('does not handle error tooltip', () => {
+ expect(show).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `true`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'true',
+ });
+ init();
+ emitErrorEvent();
+ });
+
+ itHandlesTooltip(I18N_ERROR_MESSAGE);
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is not set', () => {
+ beforeEach(() => {
+ createButton();
+ init();
+ emitErrorEvent();
+ });
+
+ itHandlesTooltip(I18N_ERROR_MESSAGE);
+ });
+ });
+});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
index 46a5631b028..d698ee72ea4 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
@@ -20,12 +20,6 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
foo/bar/dummy.md
</strong>
- <small
- class="mr-2"
- >
- a lot
- </small>
-
<clipboard-button-stub
category="tertiary"
cssclass="btn-clipboard btn-transparent lh-100 position-static"
@@ -36,5 +30,13 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
tooltipplacement="top"
variant="default"
/>
+
+ <small
+ class="mr-2"
+ >
+ a lot
+ </small>
+
+ <!---->
</div>
`;
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index db9684239a1..22bec77276b 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -17,7 +17,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
</div>
<div
- class="gl-display-none gl-sm-display-flex"
+ class="gl-sm-display-flex file-actions"
>
<viewer-switcher-stub
value="simple"
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index ac3080c65a5..910fc5c946d 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -44,6 +44,8 @@ describe('Blob Header Editing', () => {
const inputComponent = wrapper.find(GlFormInput);
const newValue = 'bar.txt';
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
name: newValue,
});
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index d935f73c0d1..8220b598ff6 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -1,3 +1,4 @@
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BlobHeaderFilepath from '~/blob/components/blob_header_filepath.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -24,6 +25,8 @@ describe('Blob Header Filepath', () => {
wrapper.destroy();
});
+ const findBadge = () => wrapper.find(GlBadge);
+
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
@@ -54,6 +57,11 @@ describe('Blob Header Filepath', () => {
expect(wrapper.vm.blobSize).toBe('a lot');
});
+ it('renders LFS badge if LFS if enabled', () => {
+ createComponent({ storedExternally: true, externalStorage: 'lfs' });
+ expect(findBadge().text()).toBe('LFS');
+ });
+
it('renders a slot and prepends its contents to the existing one', () => {
const slotContent = 'Foo Bar';
createComponent(
diff --git a/spec/frontend/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index 97ae6c0e3b7..330f1f3137e 100644
--- a/spec/frontend/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery';
+import LineHighlighter from '~/blob/line_highlighter';
import * as utils from '~/lib/utils/common_utils';
-import LineHighlighter from '~/line_highlighter';
describe('LineHighlighter', () => {
const testContext = {};
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 061ac7ad167..9e9f866d40c 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -21,6 +21,7 @@ describe('Blob viewer', () => {
setTestTimeout(2000);
beforeEach(() => {
+ window.gon.features = { refactorBlobViewer: false }; // This file is based on the old (non-refactored) blob viewer
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
$.fn.extend(jQueryMock);
mock = new MockAdapter(axios);
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 5742dfdc5d2..3af173aa18c 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -167,7 +167,7 @@ describe('Board card', () => {
mountComponent({ item: { ...mockIssue, isLoading: true } });
expect(wrapper.classes()).toContain('is-disabled');
- expect(wrapper.classes()).not.toContain('user-can-drag');
+ expect(wrapper.classes()).not.toContain('gl-cursor-grab');
});
});
@@ -177,7 +177,7 @@ describe('Board card', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('is-disabled');
- expect(wrapper.classes()).toContain('user-can-drag');
+ expect(wrapper.classes()).toContain('gl-cursor-grab');
});
});
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 7b176cea2a3..368c7d561f8 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -9,6 +9,7 @@ import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
+import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
@@ -96,7 +97,7 @@ describe('BoardContentSidebar', () => {
});
it('confirms we render MountingPortal', () => {
- expect(wrapper.find(MountingPortal).props()).toMatchObject({
+ expect(wrapper.findComponent(MountingPortal).props()).toMatchObject({
mountTo: '#js-right-sidebar-portal',
append: true,
name: 'board-content-sidebar',
@@ -141,6 +142,10 @@ describe('BoardContentSidebar', () => {
);
});
+ it('does not render SidebarSeverity', () => {
+ expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(false);
+ });
+
describe('when we emit close', () => {
let toggleBoardItem;
@@ -160,4 +165,17 @@ describe('BoardContentSidebar', () => {
});
});
});
+
+ describe('incident sidebar', () => {
+ beforeEach(() => {
+ createStore({
+ mockGetters: { activeBoardItem: () => ({ ...mockIssue, epic: null, type: 'INCIDENT' }) },
+ });
+ createComponent();
+ });
+
+ it('renders SidebarSeverity', () => {
+ expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index ea551e94f2f..a8398a138ba 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -118,6 +118,7 @@ describe('BoardFilteredSearch', () => {
it('sets the url params to the correct results', async () => {
const mockFilters = [
{ type: 'author', value: { data: 'root', operator: '=' } },
+ { type: 'assignee', value: { data: 'root', operator: '=' } },
{ type: 'label', value: { data: 'label', operator: '=' } },
{ type: 'label', value: { data: 'label2', operator: '=' } },
{ type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
@@ -133,7 +134,26 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
- 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
+ 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&assignee_username=root&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
+ });
+ });
+
+ describe('when assignee is passed a wildcard value', () => {
+ const url = (arg) => `http://test.host/?assignee_id=${arg}`;
+
+ it.each([
+ ['None', url('None')],
+ ['Any', url('Any')],
+ ])('sets the url param %s', (assigneeParam, expected) => {
+ const mockFilters = [{ type: 'assignee', value: { data: assigneeParam, operator: '=' } }];
+ jest.spyOn(urlUtility, 'updateHistory');
+ findFilteredSearch().vm.$emit('onFilter', mockFilters);
+
+ expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ title: '',
+ replace: true,
+ url: expected,
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 148d0c5684d..8cc0ad5f30c 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -180,18 +180,18 @@ describe('Board List Header Component', () => {
const canDragList = [ListType.label, ListType.milestone, ListType.iteration, ListType.assignee];
it.each(cannotDragList)(
- 'does not have user-can-drag-class so user cannot drag list',
+ 'does not have gl-cursor-grab class so user cannot drag list',
(listType) => {
createComponent({ listType });
- expect(findTitle().classes()).not.toContain('user-can-drag');
+ expect(findTitle().classes()).not.toContain('gl-cursor-grab');
},
);
- it.each(canDragList)('has user-can-drag-class so user can drag list', (listType) => {
+ it.each(canDragList)('has gl-cursor-grab class so user can drag list', (listType) => {
createComponent({ listType });
- expect(findTitle().classes()).toContain('user-can-drag');
+ expect(findTitle().classes()).toContain('gl-cursor-grab');
});
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index c841c17a029..9cf7c5774bf 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -96,6 +96,8 @@ describe('BoardsSelector', () => {
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
[options.loadingKey]: true,
});
@@ -161,6 +163,8 @@ describe('BoardsSelector', () => {
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
loadingBoards: false,
loadingRecentBoards: false,
@@ -176,6 +180,8 @@ describe('BoardsSelector', () => {
describe('filtering', () => {
beforeEach(async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
boards,
});
@@ -208,6 +214,8 @@ describe('BoardsSelector', () => {
describe('recent boards section', () => {
it('shows only when boards are greater than 10', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
boards,
});
@@ -217,6 +225,8 @@ describe('BoardsSelector', () => {
});
it('does not show when boards are less than 10', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
boards: boards.slice(0, 5),
});
@@ -226,6 +236,8 @@ describe('BoardsSelector', () => {
});
it('does not show when recentBoards api returns empty array', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
recentBoards: [],
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 51340a3ea4f..7c842d71688 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -29,6 +29,8 @@ import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
+import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
import {
mockLists,
mockListsById,
@@ -308,6 +310,36 @@ describe('fetchMilestones', () => {
expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type'));
});
+ it.each([
+ [
+ 'project',
+ {
+ query: projectBoardMilestones,
+ variables: { fullPath: 'gitlab-org/gitlab', state: 'active' },
+ },
+ ],
+ [
+ 'group',
+ {
+ query: groupBoardMilestones,
+ variables: { fullPath: 'gitlab-org/gitlab', state: 'active' },
+ },
+ ],
+ ])(
+ 'when boardType is %s it calls fetchMilestones with the correct query and variables',
+ (boardType, variables) => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ const store = createStore();
+
+ store.state.boardType = boardType;
+
+ actions.fetchMilestones(store);
+
+ expect(gqlClient.query).toHaveBeenCalledWith(variables);
+ },
+ );
+
it('sets milestonesLoading to true', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
diff --git a/spec/frontend/branches/branches_delete_modal_spec.js b/spec/frontend/branches/branches_delete_modal_spec.js
deleted file mode 100644
index 8b10cca7a11..00000000000
--- a/spec/frontend/branches/branches_delete_modal_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'jquery';
-import DeleteModal from '~/branches/branches_delete_modal';
-
-describe('branches delete modal', () => {
- describe('setDisableDeleteButton', () => {
- let submitSpy;
- let $deleteButton;
-
- beforeEach(() => {
- setFixtures(`
- <div id="modal-delete-branch">
- <form>
- <button type="submit" class="js-delete-branch">Delete</button>
- </form>
- </div>
- `);
- $deleteButton = $('.js-delete-branch');
- submitSpy = jest.fn((event) => event.preventDefault());
- $('#modal-delete-branch form').on('submit', submitSpy);
- // eslint-disable-next-line no-new
- new DeleteModal();
- });
-
- it('does not submit if button is disabled', () => {
- $deleteButton.attr('disabled', true);
-
- $deleteButton.click();
-
- expect(submitSpy).not.toHaveBeenCalled();
- });
-
- it('submits if button is not disabled', () => {
- $deleteButton.attr('disabled', false);
-
- $deleteButton.click();
-
- expect(submitSpy).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js
index 70d116c12d3..c4b2927764e 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci_lint/components/ci_lint_spec.js
@@ -66,6 +66,8 @@ describe('CI Lint', () => {
it('validate action calls mutation with dry run', async () => {
const dryRunEnabled = true;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ dryRun: dryRunEnabled });
findValidateBtn().vm.$emit('click');
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index d5a8117f48c..2a3c11f4b47 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -19,7 +19,7 @@ describe('ClusterAgentShow', () => {
let wrapper;
useFakeDate([2021, 2, 15]);
- const propsData = {
+ const provide = {
agentName: 'cluster-agent',
projectPath: 'path/to/project',
};
@@ -49,7 +49,7 @@ describe('ClusterAgentShow', () => {
shallowMount(ClusterAgentShow, {
localVue,
apolloProvider,
- propsData,
+ provide,
stubs: { GlSprintf, TimeAgoTooltip, GlTab },
}),
);
@@ -60,7 +60,7 @@ describe('ClusterAgentShow', () => {
wrapper = extendedWrapper(
shallowMount(ClusterAgentShow, {
- propsData,
+ provide,
mocks: { $apollo, clusterAgent },
slots,
stubs: { GlTab },
@@ -85,7 +85,7 @@ describe('ClusterAgentShow', () => {
});
it('displays the agent name', () => {
- expect(wrapper.text()).toContain(propsData.agentName);
+ expect(wrapper.text()).toContain(provide.agentName);
});
it('displays agent create information', () => {
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index b129baa2d83..d041cd1e164 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -82,6 +82,8 @@ describe('ClusterIntegrationForm', () => {
.then(() => {
// setData is a bad approach because it changes the internal implementation which we should not touch
// but our GlFormInput lacks the ability to set a new value.
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
})
.then(() => {
@@ -93,6 +95,8 @@ describe('ClusterIntegrationForm', () => {
return wrapper.vm
.$nextTick()
.then(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
})
.then(() => {
diff --git a/spec/frontend/clusters_list/components/agent_options_spec.js b/spec/frontend/clusters_list/components/agent_options_spec.js
new file mode 100644
index 00000000000..05bab247816
--- /dev/null
+++ b/spec/frontend/clusters_list/components/agent_options_spec.js
@@ -0,0 +1,211 @@
+import { GlDropdown, GlDropdownItem, GlModal, GlFormInput } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { ENTER_KEY } from '~/lib/utils/keys';
+import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
+import deleteAgentMutation from '~/clusters_list/graphql/mutations/delete_agent.mutation.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import AgentOptions from '~/clusters_list/components/agent_options.vue';
+import { MAX_LIST_COUNT } from '~/clusters_list/constants';
+import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
+
+Vue.use(VueApollo);
+
+const projectPath = 'path/to/project';
+const defaultBranchName = 'default';
+const maxAgents = MAX_LIST_COUNT;
+const agent = {
+ id: 'agent-id',
+ name: 'agent-name',
+ webPath: 'agent-webPath',
+};
+
+describe('AgentOptions', () => {
+ let wrapper;
+ let toast;
+ let apolloProvider;
+ let deleteResponse;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteBtn = () => wrapper.findComponent(GlDropdownItem);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+
+ const createMockApolloProvider = ({ mutationResponse }) => {
+ deleteResponse = jest.fn().mockResolvedValue(mutationResponse);
+
+ return createMockApollo([[deleteAgentMutation, deleteResponse]]);
+ };
+
+ const writeQuery = () => {
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getAgentsQuery,
+ variables: {
+ projectPath,
+ defaultBranchName,
+ first: maxAgents,
+ last: null,
+ },
+ data: getAgentResponse.data,
+ });
+ };
+
+ const createWrapper = ({ mutationResponse = mockDeleteResponse } = {}) => {
+ apolloProvider = createMockApolloProvider({ mutationResponse });
+ const provide = {
+ projectPath,
+ };
+ const propsData = {
+ defaultBranchName,
+ maxAgents,
+ agent,
+ };
+
+ toast = jest.fn();
+
+ wrapper = shallowMountExtended(AgentOptions, {
+ apolloProvider,
+ provide,
+ propsData,
+ mocks: { $toast: { show: toast } },
+ stubs: { GlModal },
+ });
+ wrapper.vm.$refs.modal.hide = jest.fn();
+
+ writeQuery();
+ return wrapper.vm.$nextTick();
+ };
+
+ const submitAgentToDelete = async () => {
+ findDeleteBtn().vm.$emit('click');
+ findInput().vm.$emit('input', agent.name);
+ await findModal().vm.$emit('primary');
+ };
+
+ beforeEach(() => {
+ return createWrapper({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ apolloProvider = null;
+ deleteResponse = null;
+ toast = null;
+ });
+
+ describe('delete agent action', () => {
+ it('displays a delete button', () => {
+ expect(findDeleteBtn().text()).toBe('Delete agent');
+ });
+
+ describe('when clicking the delete button', () => {
+ beforeEach(() => {
+ findDeleteBtn().vm.$emit('click');
+ });
+
+ it('displays a delete confirmation modal', () => {
+ expect(findModal().isVisible()).toBe(true);
+ });
+ });
+
+ describe.each`
+ condition | agentName | isDisabled | mutationCalled
+ ${'the input with agent name is missing'} | ${''} | ${true} | ${false}
+ ${'the input with agent name is incorrect'} | ${'wrong-name'} | ${true} | ${false}
+ ${'the input with agent name is correct'} | ${agent.name} | ${false} | ${true}
+ `('when $condition', ({ agentName, isDisabled, mutationCalled }) => {
+ beforeEach(() => {
+ findDeleteBtn().vm.$emit('click');
+ findInput().vm.$emit('input', agentName);
+ });
+
+ it(`${isDisabled ? 'disables' : 'enables'} the modal primary button`, () => {
+ expect(findPrimaryActionAttributes('disabled')).toBe(isDisabled);
+ });
+
+ describe('when user clicks the modal primary button', () => {
+ beforeEach(async () => {
+ await findModal().vm.$emit('primary');
+ });
+
+ if (mutationCalled) {
+ it('calls the delete mutation', () => {
+ expect(deleteResponse).toHaveBeenCalledWith({ input: { id: agent.id } });
+ });
+ } else {
+ it("doesn't call the delete mutation", () => {
+ expect(deleteResponse).not.toHaveBeenCalled();
+ });
+ }
+ });
+
+ describe('when user presses the enter button', () => {
+ beforeEach(async () => {
+ await findInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ });
+
+ if (mutationCalled) {
+ it('calls the delete mutation', () => {
+ expect(deleteResponse).toHaveBeenCalledWith({ input: { id: agent.id } });
+ });
+ } else {
+ it("doesn't call the delete mutation", () => {
+ expect(deleteResponse).not.toHaveBeenCalled();
+ });
+ }
+ });
+ });
+
+ describe('when agent was deleted successfully', () => {
+ beforeEach(async () => {
+ await submitAgentToDelete();
+ });
+
+ it('calls the toast action', () => {
+ expect(toast).toHaveBeenCalledWith(`${agent.name} successfully deleted`);
+ });
+ });
+ });
+
+ describe('when getting an error deleting agent', () => {
+ beforeEach(async () => {
+ await createWrapper({ mutationResponse: mockErrorDeleteResponse });
+
+ submitAgentToDelete();
+ });
+
+ it('displays the error message', () => {
+ expect(toast).toHaveBeenCalledWith('could not delete agent');
+ });
+ });
+
+ describe('when the delete modal was closed', () => {
+ beforeEach(async () => {
+ const loadingResponse = new Promise(() => {});
+ await createWrapper({ mutationResponse: loadingResponse });
+
+ submitAgentToDelete();
+ });
+
+ it('reenables the options dropdown', async () => {
+ expect(findPrimaryActionAttributes('loading')).toBe(true);
+ expect(findDropdown().attributes('disabled')).toBe('true');
+
+ await findModal().vm.$emit('hide');
+
+ expect(findPrimaryActionAttributes('loading')).toBe(false);
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
+ });
+
+ it('clears the agent name input', async () => {
+ expect(findInput().attributes('value')).toBe(agent.name);
+
+ await findModal().vm.$emit('hide');
+
+ expect(findInput().attributes('value')).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index a6d76b069cf..887c17bb4ad 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -1,16 +1,22 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from '~/clusters_list/components/agent_table.vue';
+import AgentOptions from '~/clusters_list/components/agent_options.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const connectedTimeNow = new Date();
const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+const provideData = {
+ projectPath: 'path/to/project',
+};
const propsData = {
agents: [
{
name: 'agent-1',
+ id: 'agent-1-id',
configFolder: {
webPath: '/agent/full/path',
},
@@ -21,6 +27,7 @@ const propsData = {
},
{
name: 'agent-2',
+ id: 'agent-2-id',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
@@ -34,6 +41,7 @@ const propsData = {
},
{
name: 'agent-3',
+ id: 'agent-3-id',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
@@ -48,6 +56,10 @@ const propsData = {
],
};
+const AgentOptionsStub = stubComponent(AgentOptions, {
+ template: `<div></div>`,
+});
+
describe('AgentTable', () => {
let wrapper;
@@ -57,15 +69,21 @@ describe('AgentTable', () => {
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
+ const findAgentOptions = () => wrapper.findAllComponents(AgentOptions);
beforeEach(() => {
- wrapper = mountExtended(AgentTable, { propsData });
+ wrapper = mountExtended(AgentTable, {
+ propsData,
+ provide: provideData,
+ stubs: {
+ AgentOptions: AgentOptionsStub,
+ },
+ });
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
@@ -108,5 +126,9 @@ describe('AgentTable', () => {
expect(findLink.exists()).toBe(hasLink);
expect(findConfiguration(lineNumber).text()).toBe(agentPath);
});
+
+ it('displays actions menu for each agent', () => {
+ expect(findAgentOptions()).toHaveLength(3);
+ });
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index a34202c789d..9af25a534d8 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -272,6 +272,8 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2));
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentPage: 2 });
return axios.waitForAll();
});
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
index 804f9834506..c4a31ed4394 100644
--- a/spec/frontend/clusters_list/mocks/apollo.js
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -75,3 +75,15 @@ export const getAgentResponse = {
},
},
};
+
+export const mockDeleteResponse = {
+ data: { clusterAgentDelete: { errors: [] } },
+};
+
+export const mockErrorDeleteResponse = {
+ data: {
+ clusterAgentDelete: {
+ errors: ['could not delete agent'],
+ },
+ },
+};
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index c376b58cc72..e209f628aa2 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -92,6 +92,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should make an API request when using pagination', async () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
store: {
state: {
diff --git a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
index de8f8efd260..415f1314a36 100644
--- a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
@@ -26,6 +26,11 @@ describe('content/components/wrappers/frontmatter', () => {
expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
});
+ it('adds content-editor-code-block class to the pre element', () => {
+ createWrapper();
+ expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('content-editor-code-block');
+ });
+
it('renders a node-view-content as a code element', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index 6a0a0c76825..05fa0f79ef0 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -36,4 +36,10 @@ describe('content_editor/extensions/code_block_highlight', () => {
expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
});
+
+ it('adds content-editor-code-block class to the pre element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+
+ expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ });
});
diff --git a/spec/frontend/content_editor/extensions/code_spec.js b/spec/frontend/content_editor/extensions/code_spec.js
new file mode 100644
index 00000000000..0a54ac6a96b
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/code_spec.js
@@ -0,0 +1,8 @@
+import Code from '~/content_editor/extensions/code';
+import { EXTENSION_PRIORITY_LOWER } from '~/content_editor/constants';
+
+describe('content_editor/extensions/code', () => {
+ it('has a lower loading priority', () => {
+ expect(Code.config.priority).toBe(EXTENSION_PRIORITY_LOWER);
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js
index 517f6947b9a..a8cbad6ef81 100644
--- a/spec/frontend/content_editor/extensions/frontmatter_spec.js
+++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js
@@ -1,30 +1,47 @@
import Frontmatter from '~/content_editor/extensions/frontmatter';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/frontmatter', () => {
let tiptapEditor;
let doc;
- let p;
+ let frontmatter;
+ let codeBlock;
beforeEach(() => {
- tiptapEditor = createTestEditor({ extensions: [Frontmatter] });
+ tiptapEditor = createTestEditor({ extensions: [Frontmatter, CodeBlockHighlight] });
({
- builders: { doc, p },
+ builders: { doc, codeBlock, frontmatter },
} = createDocBuilder({
tiptapEditor,
names: {
frontmatter: { nodeType: Frontmatter.name },
+ codeBlock: { nodeType: CodeBlockHighlight.name },
},
}));
});
it('does not insert a frontmatter block when executing code block input rule', () => {
- const expectedDoc = doc(p(''));
+ const expectedDoc = doc(codeBlock(''));
const inputRuleText = '``` ';
triggerNodeInputRule({ tiptapEditor, inputRuleText });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
+
+ it.each`
+ command | result | resultDesc
+ ${'toggleCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}
+ ${'setCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}
+ ${'setFrontmatter'} | ${() => doc(frontmatter(''))} | ${'frontmatter element'}
+ ${'toggleFrontmatter'} | ${() => doc(frontmatter(''))} | ${'frontmatter element'}
+ `('executing $command should generate a document with a $resultDesc', ({ command, result }) => {
+ const expectedDoc = result();
+
+ tiptapEditor.commands[command]();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
});
diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js
new file mode 100644
index 00000000000..256f7bad309
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -0,0 +1,41 @@
+import Image from '~/content_editor/extensions/image';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/image', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let image;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Image] });
+
+ ({
+ builders: { doc, p, image },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ image: { nodeType: Image.name },
+ },
+ }));
+ });
+
+ it('adds data-canonical-src attribute when rendering to HTML', () => {
+ const initialDoc = doc(
+ p(
+ image({
+ canonicalSrc: 'uploads/image.jpg',
+ src: '/-/wikis/uploads/image.jpg',
+ alt: 'image',
+ title: 'this is an image',
+ }),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ expect(tiptapEditor.getHTML()).toEqual(
+ '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image" data-canonical-src="uploads/image.jpg"></p>',
+ );
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index ead898554d1..bb841357d37 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -33,7 +33,7 @@ describe('content_editor/extensions/link', () => {
${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
- ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
+ ${'www.example.com '} | ${() => p(link({ href: 'http://www.example.com' }, 'www.example.com'))}
${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 97f6d8f6334..01d4c994e88 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -164,6 +164,17 @@ describe('markdownSerializer', () => {
expect(serialize(paragraph(italic('italics')))).toBe('_italics_');
});
+ it('correctly serializes code blocks wrapped by italics and bold marks', () => {
+ const text = 'code block';
+
+ expect(serialize(paragraph(italic(code(text))))).toBe(`_\`${text}\`_`);
+ expect(serialize(paragraph(code(italic(text))))).toBe(`_\`${text}\`_`);
+ expect(serialize(paragraph(bold(code(text))))).toBe(`**\`${text}\`**`);
+ expect(serialize(paragraph(code(bold(text))))).toBe(`**\`${text}\`**`);
+ expect(serialize(paragraph(strike(code(text))))).toBe(`~~\`${text}\`~~`);
+ expect(serialize(paragraph(code(strike(text))))).toBe(`~~\`${text}\`~~`);
+ });
+
it('correctly serializes inline diff', () => {
expect(
serialize(
@@ -341,6 +352,10 @@ this is not really json but just trying out whether this case works or not
);
});
+ it('does not serialize an image when src and canonicalSrc are empty', () => {
+ expect(serialize(paragraph(image({})))).toBe('');
+ });
+
it('correctly serializes an image with a title', () => {
expect(serialize(paragraph(image({ src: 'img.jpg', title: 'baz', alt: 'foo bar' })))).toBe(
'![foo bar](img.jpg "baz")',
diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
index 0c6095e601f..4e92fa1df16 100644
--- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
@@ -206,6 +206,8 @@ describe('ClusterFormDropdown', () => {
const searchQuery = secondItem.name;
wrapper.setProps({ items });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchQuery });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
index d866ffd4efb..a0510d46794 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
@@ -67,6 +67,8 @@ describe('ServiceCredentialsForm', () => {
});
it('enables submit button when role ARN is not provided', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' });
return vm.vm.$nextTick().then(() => {
@@ -75,6 +77,8 @@ describe('ServiceCredentialsForm', () => {
});
it('dispatches createRole action when submit button is clicked', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' }); // set role ARN to enable button
findSubmitButton().vm.$emit('click', new Event('click'));
@@ -84,6 +88,8 @@ describe('ServiceCredentialsForm', () => {
describe('when is creating role', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' }); // set role ARN to enable button
state.isCreatingRole = true;
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
index 8f4903dd91b..2b6f2134553 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
@@ -79,6 +79,8 @@ describe('GkeMachineTypeDropdown', () => {
store = createStore();
wrapper = createComponent(store);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index b191b107609..2b0acc8cf5d 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -83,6 +83,8 @@ describe('GkeProjectIdDropdown', () => {
it('returns default toggle text', () => {
bootstrap();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
@@ -99,6 +101,8 @@ describe('GkeProjectIdDropdown', () => {
hasProject: () => true,
},
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
@@ -110,6 +114,8 @@ describe('GkeProjectIdDropdown', () => {
bootstrap({
projects: null,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
index 4054b768e34..22fc681f863 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
@@ -47,6 +47,8 @@ describe('GkeZoneDropdown', () => {
describe('isLoading', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/crm/contact_form_spec.js b/spec/frontend/crm/contact_form_spec.js
index b2753ad8cf5..0edab4f5ec5 100644
--- a/spec/frontend/crm/contact_form_spec.js
+++ b/spec/frontend/crm/contact_form_spec.js
@@ -112,7 +112,7 @@ describe('Customer relations contact form component', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Phone is invalid.');
+ expect(findError().text()).toBe('create contact is invalid.');
});
});
@@ -151,7 +151,7 @@ describe('Customer relations contact form component', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Email is invalid.');
+ expect(findError().text()).toBe('update contact is invalid.');
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
new file mode 100644
index 00000000000..0e3abc05c37
--- /dev/null
+++ b/spec/frontend/crm/form_spec.js
@@ -0,0 +1,278 @@
+import { GlAlert } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Form from '~/crm/components/form.vue';
+import routes from '~/crm/routes';
+import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
+import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
+import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
+import createOrganizationMutation from '~/crm/components/queries/create_organization.mutation.graphql';
+import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import {
+ createContactMutationErrorResponse,
+ createContactMutationResponse,
+ getGroupContactsQueryResponse,
+ updateContactMutationErrorResponse,
+ updateContactMutationResponse,
+ createOrganizationMutationErrorResponse,
+ createOrganizationMutationResponse,
+ getGroupOrganizationsQueryResponse,
+} from './mock_data';
+
+const FORM_CREATE_CONTACT = 'create contact';
+const FORM_UPDATE_CONTACT = 'update contact';
+const FORM_CREATE_ORG = 'create organization';
+
+describe('Reusable form component', () => {
+ Vue.use(VueApollo);
+ Vue.use(VueRouter);
+
+ const DEFAULT_RESPONSES = {
+ createContact: Promise.resolve(createContactMutationResponse),
+ updateContact: Promise.resolve(updateContactMutationResponse),
+ createOrg: Promise.resolve(createOrganizationMutationResponse),
+ };
+
+ let wrapper;
+ let handler;
+ let fakeApollo;
+ let router;
+
+ beforeEach(() => {
+ router = new VueRouter({
+ base: '',
+ mode: 'history',
+ routes,
+ });
+ router.push('/test');
+
+ handler = jest.fn().mockImplementation((key) => DEFAULT_RESPONSES[key]);
+
+ const hanlderWithKey = (key) => (...args) => handler(key, ...args);
+
+ fakeApollo = createMockApollo([
+ [createContactMutation, hanlderWithKey('createContact')],
+ [updateContactMutation, hanlderWithKey('updateContact')],
+ [createOrganizationMutation, hanlderWithKey('createOrg')],
+ ]);
+
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ data: getGroupContactsQueryResponse.data,
+ });
+
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ data: getGroupOrganizationsQueryResponse.data,
+ });
+ });
+
+ const mockToastShow = jest.fn();
+
+ const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findForm = () => wrapper.find('form');
+ const findError = () => wrapper.findComponent(GlAlert);
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMountExtended(Form, {
+ router,
+ apolloProvider: fakeApollo,
+ propsData: { drawerOpen: true, ...propsData },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+ };
+
+ const mountContact = ({ propsData } = {}) => {
+ mountComponent({
+ fields: [
+ { name: 'firstName', label: 'First name', required: true },
+ { name: 'lastName', label: 'Last name', required: true },
+ { name: 'email', label: 'Email', required: true },
+ { name: 'phone', label: 'Phone' },
+ { name: 'description', label: 'Description' },
+ ],
+ ...propsData,
+ });
+ };
+
+ const mountContactCreate = () => {
+ const propsData = {
+ title: 'New contact',
+ successMessage: 'Contact has been added',
+ buttonLabel: 'Create contact',
+ getQuery: {
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.contacts',
+ mutation: createContactMutation,
+ additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
+ };
+ mountContact({ propsData });
+ };
+
+ const mountContactUpdate = () => {
+ const propsData = {
+ title: 'Edit contact',
+ successMessage: 'Contact has been updated',
+ mutation: updateContactMutation,
+ existingModel: {
+ id: 'gid://gitlab/CustomerRelations::Contact/12',
+ firstName: 'First',
+ lastName: 'Last',
+ email: 'email@example.com',
+ },
+ };
+ mountContact({ propsData });
+ };
+
+ const mountOrganization = ({ propsData } = {}) => {
+ mountComponent({
+ fields: [
+ { name: 'name', label: 'Name', required: true },
+ { name: 'defaultRate', label: 'Default rate', input: { type: 'number', step: '0.01' } },
+ { name: 'description', label: 'Description' },
+ ],
+ ...propsData,
+ });
+ };
+
+ const mountOrganizationCreate = () => {
+ const propsData = {
+ title: 'New organization',
+ successMessage: 'Organization has been added',
+ buttonLabel: 'Create organization',
+ getQuery: {
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.organizations',
+ mutation: createOrganizationMutation,
+ additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
+ };
+ mountOrganization({ propsData });
+ };
+
+ const forms = {
+ [FORM_CREATE_CONTACT]: {
+ mountFunction: mountContactCreate,
+ mutationErrorResponse: createContactMutationErrorResponse,
+ toastMessage: 'Contact has been added',
+ },
+ [FORM_UPDATE_CONTACT]: {
+ mountFunction: mountContactUpdate,
+ mutationErrorResponse: updateContactMutationErrorResponse,
+ toastMessage: 'Contact has been updated',
+ },
+ [FORM_CREATE_ORG]: {
+ mountFunction: mountOrganizationCreate,
+ mutationErrorResponse: createOrganizationMutationErrorResponse,
+ toastMessage: 'Organization has been added',
+ },
+ };
+ const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT))(
+ '%s form save button',
+ (name, { mountFunction }) => {
+ beforeEach(() => {
+ mountFunction();
+ });
+
+ it('should be disabled when required fields are empty', async () => {
+ wrapper.find('#firstName').vm.$emit('input', '');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('should not be disabled when required fields have values', async () => {
+ wrapper.find('#firstName').vm.$emit('input', 'A');
+ wrapper.find('#lastName').vm.$emit('input', 'B');
+ wrapper.find('#email').vm.$emit('input', 'C');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ },
+ );
+
+ describe.each(asTestParams(FORM_CREATE_ORG))('%s form save button', (name, { mountFunction }) => {
+ beforeEach(() => {
+ mountFunction();
+ });
+
+ it('should be disabled when required field is empty', async () => {
+ wrapper.find('#name').vm.$emit('input', '');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('should not be disabled when required field has a value', async () => {
+ wrapper.find('#name').vm.$emit('input', 'A');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT, FORM_CREATE_ORG))(
+ 'when %s mutation is successful',
+ (name, { mountFunction, toastMessage }) => {
+ it('form should display correct toast message', async () => {
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(toastMessage);
+ });
+ },
+ );
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT, FORM_CREATE_ORG))(
+ 'when %s mutation fails',
+ (formName, { mutationErrorResponse, mountFunction }) => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
+ it('should show error on reject', async () => {
+ handler.mockRejectedValue('ERROR');
+
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(findError().text()).toBe('Something went wrong. Please try again.');
+ });
+
+ it('should show error on error response', async () => {
+ handler.mockResolvedValue(mutationErrorResponse);
+
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(findError().text()).toBe(`${formName} is invalid.`);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
index f7af2ccdb72..e351e101b29 100644
--- a/spec/frontend/crm/mock_data.js
+++ b/spec/frontend/crm/mock_data.js
@@ -82,7 +82,6 @@ export const getGroupOrganizationsQueryResponse = {
export const createContactMutationResponse = {
data: {
customerRelationsContactCreate: {
- __typeName: 'CustomerRelationsContactCreatePayload',
contact: {
__typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/1',
@@ -102,7 +101,7 @@ export const createContactMutationErrorResponse = {
data: {
customerRelationsContactCreate: {
contact: null,
- errors: ['Phone is invalid.'],
+ errors: ['create contact is invalid.'],
},
},
};
@@ -130,7 +129,7 @@ export const updateContactMutationErrorResponse = {
data: {
customerRelationsContactUpdate: {
contact: null,
- errors: ['Email is invalid.'],
+ errors: ['update contact is invalid.'],
},
},
};
@@ -138,7 +137,6 @@ export const updateContactMutationErrorResponse = {
export const createOrganizationMutationResponse = {
data: {
customerRelationsOrganizationCreate: {
- __typeName: 'CustomerRelationsOrganizationCreatePayload',
organization: {
__typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/2',
@@ -155,7 +153,7 @@ export const createOrganizationMutationErrorResponse = {
data: {
customerRelationsOrganizationCreate: {
organization: null,
- errors: ['Name cannot be blank.'],
+ errors: ['create organization is invalid.'],
},
},
};
diff --git a/spec/frontend/crm/new_organization_form_spec.js b/spec/frontend/crm/new_organization_form_spec.js
index 976b626f35f..0a7909774c9 100644
--- a/spec/frontend/crm/new_organization_form_spec.js
+++ b/spec/frontend/crm/new_organization_form_spec.js
@@ -103,7 +103,7 @@ describe('Customer relations organizations root app', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Name cannot be blank.');
+ expect(findError().text()).toBe('create organization is invalid.');
});
});
});
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 3158446c37d..9605dce2668 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -24,6 +24,7 @@ const findTable = () => wrapper.findComponent(GlTable);
const findTableHead = () => wrapper.find('thead');
const findTableHeadColumns = () => findTableHead().findAll('th');
const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title');
+const findStageEventLink = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-link');
const findStageTime = () => wrapper.findByTestId('vsa-stage-event-time');
const findIcon = (name) => wrapper.findByTestId(`${name}-icon`);
@@ -86,6 +87,15 @@ describe('StageTable', () => {
expect(titles[index]).toBe(ev.title);
});
});
+
+ it('will not display the project name in the record link', () => {
+ const evs = findStageEvents();
+
+ const links = evs.wrappers.map((ev) => findStageEventLink(ev).text());
+ issueEventItems.forEach((ev, index) => {
+ expect(links[index]).toBe(`#${ev.iid}`);
+ });
+ });
});
describe('default event', () => {
@@ -187,6 +197,53 @@ describe('StageTable', () => {
});
});
+ describe('includeProjectName set', () => {
+ const fakenamespace = 'some/fake/path';
+ beforeEach(() => {
+ wrapper = createComponent({ includeProjectName: true });
+ });
+
+ it('will display the project name in the record link', () => {
+ const evs = findStageEvents();
+
+ const links = evs.wrappers.map((ev) => findStageEventLink(ev).text());
+ issueEventItems.forEach((ev, index) => {
+ expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`);
+ });
+ });
+
+ describe.each`
+ namespaceFullPath | hasFullPath
+ ${'fake'} | ${false}
+ ${fakenamespace} | ${true}
+ `('with a namespace', ({ namespaceFullPath, hasFullPath }) => {
+ let evs = null;
+ let links = null;
+
+ beforeEach(() => {
+ wrapper = createComponent({
+ includeProjectName: true,
+ stageEvents: issueEventItems.map((ie) => ({ ...ie, namespaceFullPath })),
+ });
+
+ evs = findStageEvents();
+ links = evs.wrappers.map((ev) => findStageEventLink(ev).text());
+ });
+
+ it(`with namespaceFullPath='${namespaceFullPath}' ${
+ hasFullPath ? 'will' : 'does not'
+ } include the namespace`, () => {
+ issueEventItems.forEach((ev, index) => {
+ if (hasFullPath) {
+ expect(links[index]).toBe(`${namespaceFullPath}/${ev.projectPath}#${ev.iid}`);
+ } else {
+ expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`);
+ }
+ });
+ });
+ });
+ });
+
describe('Pagination', () => {
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index c97e4845bc2..082db2cc312 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -63,6 +63,8 @@ describe('ValueStreamMetrics', () => {
it('renders hidden GlSingleStat components for each metric', async () => {
await waitForPromises();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
index 4dd5c29a917..5f4d4071f29 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -26,6 +26,8 @@ describe('Deploy freeze timezone dropdown', () => {
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchTerm });
};
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
new file mode 100644
index 00000000000..ab37cb90bd3
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to reply to a discussion 1`] = `
+<div
+ class="disabled-comment text-center"
+>
+ Please
+ <gl-link-stub
+ href="/users/sign_up?redirect_to_referer=yes"
+ >
+ register
+ </gl-link-stub>
+ or
+ <gl-link-stub
+ href="/users/sign_in?redirect_to_referer=yes"
+ >
+ sign in
+ </gl-link-stub>
+ to reply.
+</div>
+`;
+
+exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to start a new discussion 1`] = `
+<div
+ class="disabled-comment text-center"
+>
+ Please
+ <gl-link-stub
+ href="/users/sign_up?redirect_to_referer=yes"
+ >
+ register
+ </gl-link-stub>
+ or
+ <gl-link-stub
+ href="/users/sign_in?redirect_to_referer=yes"
+ >
+ sign in
+ </gl-link-stub>
+ to start a new discussion.
+</div>
+`;
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 9335d800a16..e816a05ba53 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,7 +1,9 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { ApolloMutation } from 'vue-apollo';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
+import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
@@ -20,6 +22,7 @@ const defaultMockDiscussion = {
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
+ const originalGon = window.gon;
let wrapper;
const findDesignNotes = () => wrapper.findAll(DesignNote);
@@ -31,6 +34,7 @@ describe('Design discussions component', () => {
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
+ const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
const mutationVariables = {
mutation: createNoteMutation,
@@ -42,6 +46,8 @@ describe('Design discussions component', () => {
},
},
};
+ const registerPath = '/users/sign_up?redirect_to_referer=yes';
+ const signInPath = '/users/sign_in?redirect_to_referer=yes';
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
const readQuery = jest.fn().mockReturnValue({
project: {
@@ -62,6 +68,8 @@ describe('Design discussions component', () => {
designId: 'design-id',
discussionIndex: 1,
discussionWithOpenForm: '',
+ registerPath,
+ signInPath,
...props,
},
data() {
@@ -88,8 +96,13 @@ describe('Design discussions component', () => {
});
}
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
describe('when discussion is not resolvable', () => {
@@ -349,4 +362,41 @@ describe('Design discussions component', () => {
expect(wrapper.emitted('open-form')).toBeTruthy();
});
+
+ describe('when user is not logged in', () => {
+ const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: null };
+ createComponent(
+ {
+ discussion: {
+ ...defaultMockDiscussion,
+ },
+ discussionWithOpenForm: defaultMockDiscussion.id,
+ },
+ { discussionComment: 'test', isFormRendered: true },
+ );
+ });
+
+ it('does not render resolve discussion button', () => {
+ expect(findResolveButton().exists()).toBe(false);
+ });
+
+ it('does not render replace-placeholder component', () => {
+ expect(findReplyPlaceholder().exists()).toBe(false);
+ });
+
+ it('does not render apollo-mutation component', () => {
+ expect(findApolloMutation().exists()).toBe(false);
+ });
+
+ it('renders design-note-signed-out component', () => {
+ expect(findDesignNoteSignedOut().exists()).toBe(true);
+ expect(findDesignNoteSignedOut().props()).toMatchObject({
+ registerPath,
+ signInPath,
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
new file mode 100644
index 00000000000..e71bb5ab520
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
@@ -0,0 +1,36 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
+
+function createComponent(isAddDiscussion = false) {
+ return shallowMount(DesignNoteSignedOut, {
+ propsData: {
+ registerPath: '/users/sign_up?redirect_to_referer=yes',
+ signInPath: '/users/sign_in?redirect_to_referer=yes',
+ isAddDiscussion,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+}
+
+describe('DesignNoteSignedOut', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders message containing register and sign-in links while user wants to reply to a discussion', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders message containing register and sign-in links while user wants to start a new discussion', () => {
+ wrapper = createComponent(true);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index d3119be7159..4bda5054090 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -117,6 +117,8 @@ describe('Design overlay component', () => {
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
'should not apply inactive class to the pin for the active discussion',
(note) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
activeDiscussion: {
id: note.id,
@@ -131,6 +133,8 @@ describe('Design overlay component', () => {
);
it('should apply inactive class to all pins besides the active one', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
activeDiscussion: {
id: notes[0].id,
@@ -212,6 +216,8 @@ describe('Design overlay component', () => {
const { position } = note;
const newCoordinates = { x: 20, y: 20 };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
movingNoteNewPosition: {
...position,
@@ -345,6 +351,8 @@ describe('Design overlay component', () => {
});
const newCoordinates = { x: 20, y: 20 };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
movingNoteStartPosition: {
...notes[0].position,
@@ -368,6 +376,8 @@ describe('Design overlay component', () => {
it('should calculate delta correctly from state', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
movingNoteStartPosition: {
clientX: 10,
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index edf8b965153..adec9ef469d 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -15,6 +15,7 @@ const mockOverlayData = {
};
describe('Design management design presentation component', () => {
+ const originalGon = window.gon;
let wrapper;
function createComponent(
@@ -39,6 +40,8 @@ describe('Design management design presentation component', () => {
stubs,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
wrapper.element.scrollTo = jest.fn();
}
@@ -113,8 +116,13 @@ describe('Design management design presentation component', () => {
});
}
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
it('renders image and overlay when image provided', () => {
@@ -550,4 +558,23 @@ describe('Design management design presentation component', () => {
});
});
});
+
+ describe('when user is not logged in', () => {
+ beforeEach(() => {
+ window.gon = { current_user_id: null };
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+ });
+
+ it('disables commenting from design overlay', () => {
+ expect(wrapper.findComponent(DesignOverlay).props()).toMatchObject({
+ disableCommenting: true,
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index 8eb993ec7b5..4cd71bdb7f3 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -2,6 +2,7 @@ import { GlCollapse, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
+import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
@@ -30,6 +31,7 @@ const cookieKey = 'hide_design_resolved_comments_popover';
const mutate = jest.fn().mockResolvedValue();
describe('Design management design sidebar component', () => {
+ const originalGon = window.gon;
let wrapper;
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
@@ -58,11 +60,20 @@ describe('Design management design sidebar component', () => {
},
},
stubs: { GlPopover },
+ provide: {
+ registerPath: '/users/sign_up?redirect_to_referer=yes',
+ signInPath: '/users/sign_in?redirect_to_referer=yes',
+ },
});
}
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
it('renders participants', () => {
@@ -248,4 +259,44 @@ describe('Design management design sidebar component', () => {
expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
});
});
+
+ describe('when user is not logged in', () => {
+ const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: null };
+ });
+
+ describe('design has no discussions', () => {
+ beforeEach(() => {
+ createComponent({
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ });
+ });
+
+ it('does not render a message about possibility to create a new discussion', () => {
+ expect(findNewDiscussionDisclaimer().exists()).toBe(false);
+ });
+
+ it('renders design-note-signed-out component', () => {
+ expect(findDesignNoteSignedOut().exists()).toBe(true);
+ });
+ });
+
+ describe('design has discussions', () => {
+ beforeEach(() => {
+ Cookies.set(cookieKey, true);
+ createComponent();
+ });
+
+ it('renders design-note-signed-out component', () => {
+ expect(findDesignNoteSignedOut().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 765d902f9a6..ac3afc73c86 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -9,6 +9,8 @@ describe('Design management large image component', () => {
wrapper = shallowMount(DesignImage, {
propsData,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
}
diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 1d9b9c002f9..6e0592984a2 100644
--- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -42,6 +42,8 @@ describe('Design management pagination component', () => {
});
it('renders navigation buttons', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
designCollection: { designs: [{ id: '1' }, { id: '2' }] },
});
@@ -53,6 +55,8 @@ describe('Design management pagination component', () => {
describe('keyboard buttons navigation', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
designCollection: { designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }] },
});
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 009ffe57744..cf872046f53 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -48,6 +48,8 @@ describe('Design management toolbar component', () => {
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
permissions: {
createDesign,
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index ebfe27eaa71..a4fb671ae13 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -34,6 +34,8 @@ describe('Design management design version dropdown component', () => {
stubs: { GlSprintf },
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions,
});
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 3d04840b1f8..31b3117cb6c 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -70,6 +70,13 @@ exports[`Design management design index page renders design index 1`] = `
<!---->
+ <design-note-signed-out-stub
+ class="gl-mb-4"
+ isadddiscussion="true"
+ registerpath=""
+ signinpath=""
+ />
+
<design-discussion-stub
data-testid="unresolved-discussion"
designid="gid::/gitlab/Design/1"
@@ -77,6 +84,8 @@ exports[`Design management design index page renders design index 1`] = `
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="gid::/gitlab/Design/1"
+ registerpath=""
+ signinpath=""
/>
<gl-button-stub
@@ -126,6 +135,8 @@ exports[`Design management design index page renders design index 1`] = `
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="gid::/gitlab/Design/1"
+ registerpath=""
+ signinpath=""
/>
</gl-collapse-stub>
@@ -231,14 +242,14 @@ exports[`Design management design index page with error GlAlert is rendered in c
participants="[object Object]"
/>
- <h2
- class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
- data-testid="new-discussion-disclaimer"
- >
-
- Click the image where you'd like to start a new discussion
-
- </h2>
+ <!---->
+
+ <design-note-signed-out-stub
+ class="gl-mb-4"
+ isadddiscussion="true"
+ registerpath=""
+ signinpath=""
+ />
<!---->
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 6ce384b4869..98e2313e9f2 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -317,6 +317,8 @@ describe('Design management design index page', () => {
describe('when no design exists for given version', () => {
it('redirects to /designs', () => {
createComponent({ loading: true });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
allVersions: mockAllVersions,
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 427161a391b..dd0f7972553 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -91,6 +91,8 @@ const designToMove = {
};
describe('Design management index page', () => {
+ const registerPath = '/users/sign_up?redirect_to_referer=yes';
+ const signInPath = '/users/sign_in?redirect_to_referer=yes';
let mutate;
let wrapper;
let fakeApollo;
@@ -164,6 +166,8 @@ describe('Design management index page', () => {
provide: {
projectPath: 'project-path',
issueIid: '1',
+ registerPath,
+ signInPath,
},
});
}
@@ -186,6 +190,10 @@ describe('Design management index page', () => {
apolloProvider: fakeApollo,
router,
stubs: { VueDraggable },
+ provide: {
+ registerPath,
+ signInPath,
+ },
});
}
@@ -204,6 +212,8 @@ describe('Design management index page', () => {
it('renders error', async () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ error: true });
await nextTick();
@@ -381,6 +391,8 @@ describe('Design management index page', () => {
it('updates state appropriately after upload complete', async () => {
createComponent({ stubs: { GlEmptyState } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
wrapper.vm.onUploadDesignDone(designUploadMutationCreatedResponse);
@@ -393,6 +405,8 @@ describe('Design management index page', () => {
it('updates state appropriately after upload error', async () => {
createComponent({ stubs: { GlEmptyState } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
wrapper.vm.onUploadDesignError();
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 47b144b2387..8c1a8041f6c 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -6,8 +6,8 @@ import { imageDiffDiscussions } from '../mock_data/diff_discussions';
describe('Diffs image diff overlay component', () => {
const dimensions = {
- width: 100,
- height: 200,
+ width: 99.9,
+ height: 199.5,
};
let wrapper;
let dispatch;
@@ -38,7 +38,6 @@ describe('Diffs image diff overlay component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
it('renders comment badges', () => {
@@ -81,17 +80,21 @@ describe('Diffs image diff overlay component', () => {
it('dispatches openDiffFileCommentForm when clicking overlay', () => {
createComponent({ canComment: true });
- wrapper.find('.js-add-image-diff-note-button').trigger('click', { offsetX: 0, offsetY: 0 });
+ wrapper.find('.js-add-image-diff-note-button').trigger('click', { offsetX: 1.2, offsetY: 3.8 });
expect(dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', {
fileHash: 'ABC',
- x: 0,
- y: 0,
+ x: 1,
+ y: 4,
width: 100,
height: 200,
- xPercent: 0,
- yPercent: 0,
+ xPercent: expect.any(Number),
+ yPercent: expect.any(Number),
});
+
+ const { xPercent, yPercent } = dispatch.mock.calls[0][1];
+ expect(xPercent).toBeCloseTo(0.6);
+ expect(yPercent).toBeCloseTo(1.9);
});
describe('toggle discussion', () => {
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index bc53202c919..049cab3a83b 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -342,27 +342,30 @@ describe('Base editor', () => {
describe('implementation', () => {
let instance;
- beforeEach(() => {
- instance = editor.createInstance({ el: editorEl, blobPath, blobContent });
- });
it('correctly proxies value from the model', () => {
+ instance = editor.createInstance({ el: editorEl, blobPath, blobContent });
expect(instance.getValue()).toBe(blobContent);
});
- it('emits the EDITOR_READY_EVENT event after setting up the instance', () => {
+ it('emits the EDITOR_READY_EVENT event passing the instance after setting it up', () => {
jest.spyOn(monacoEditor, 'create').mockImplementation(() => {
return {
setModel: jest.fn(),
onDidDispose: jest.fn(),
layout: jest.fn(),
+ dispose: jest.fn(),
};
});
- const eventSpy = jest.fn();
+ let passedInstance;
+ const eventSpy = jest.fn().mockImplementation((ev) => {
+ passedInstance = ev.detail.instance;
+ });
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
expect(eventSpy).not.toHaveBeenCalled();
- editor.createInstance({ el: editorEl });
+ instance = editor.createInstance({ el: editorEl });
expect(eventSpy).toHaveBeenCalled();
+ expect(passedInstance).toBe(instance);
});
});
diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js
index afd36a1eb88..82dc0cdc250 100644
--- a/spec/frontend/emoji/components/category_spec.js
+++ b/spec/frontend/emoji/components/category_spec.js
@@ -26,6 +26,8 @@ describe('Emoji category component', () => {
});
it('renders group', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ renderGroup: true });
expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
diff --git a/spec/frontend/emoji/components/emoji_list_spec.js b/spec/frontend/emoji/components/emoji_list_spec.js
index 9dc73ef191e..a72ba614d9f 100644
--- a/spec/frontend/emoji/components/emoji_list_spec.js
+++ b/spec/frontend/emoji/components/emoji_list_spec.js
@@ -28,6 +28,8 @@ async function factory(render, propsData = { searchValue: '' }) {
await nextTick();
if (render) {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ render: true });
// Wait for component to render
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index b699f953945..b8dcb7c0d08 100644
--- a/spec/frontend/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -26,7 +26,7 @@ describe('Confirm Rollback Modal Component', () => {
commit: {
shortId: 'abc0123',
},
- 'last?': true,
+ isLast: true,
},
modalId: 'test',
};
@@ -145,7 +145,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
- 'last?': false,
+ isLast: false,
},
},
hasMultipleCommits,
@@ -167,7 +167,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
- 'last?': false,
+ isLast: false,
},
},
hasMultipleCommits,
@@ -191,7 +191,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
- 'last?': true,
+ isLast: true,
},
},
hasMultipleCommits,
diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js
new file mode 100644
index 00000000000..37209bdc86c
--- /dev/null
+++ b/spec/frontend/environments/deployment_spec.js
@@ -0,0 +1,29 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Deployment from '~/environments/components/deployment.vue';
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+import { resolvedEnvironment } from './graphql/mock_data';
+
+describe('~/environments/components/deployment.vue', () => {
+ let wrapper;
+
+ const createWrapper = ({ propsData = {} } = {}) =>
+ mountExtended(Deployment, {
+ propsData: {
+ deployment: resolvedEnvironment.lastDeployment,
+ ...propsData,
+ },
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ describe('status', () => {
+ it('should pass the deployable status to the badge', () => {
+ wrapper = createWrapper();
+ expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe(
+ resolvedEnvironment.lastDeployment.status,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/environments/deployment_status_badge_spec.js b/spec/frontend/environments/deployment_status_badge_spec.js
new file mode 100644
index 00000000000..02aae57396a
--- /dev/null
+++ b/spec/frontend/environments/deployment_status_badge_spec.js
@@ -0,0 +1,42 @@
+import { GlBadge } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+
+describe('~/environments/components/deployment_status_badge.vue', () => {
+ let wrapper;
+
+ const createWrapper = ({ propsData = {} } = {}) =>
+ mountExtended(DeploymentStatusBadge, {
+ propsData,
+ });
+
+ describe.each`
+ status | text | variant | icon
+ ${'created'} | ${s__('Deployment|Created')} | ${'neutral'} | ${'status_created'}
+ ${'running'} | ${s__('Deployment|Running')} | ${'info'} | ${'status_running'}
+ ${'success'} | ${s__('Deployment|Success')} | ${'success'} | ${'status_success'}
+ ${'failed'} | ${s__('Deployment|Failed')} | ${'danger'} | ${'status_failed'}
+ ${'canceled'} | ${s__('Deployment|Cancelled')} | ${'neutral'} | ${'status_canceled'}
+ ${'skipped'} | ${s__('Deployment|Skipped')} | ${'neutral'} | ${'status_skipped'}
+ ${'blocked'} | ${s__('Deployment|Waiting')} | ${'neutral'} | ${'status_manual'}
+ `('$status', ({ status, text, variant, icon }) => {
+ let badge;
+
+ beforeEach(() => {
+ wrapper = createWrapper({ propsData: { status } });
+ badge = wrapper.findComponent(GlBadge);
+ });
+
+ it(`sets the text to ${text}`, () => {
+ expect(wrapper.text()).toBe(text);
+ });
+
+ it(`sets the variant to ${variant}`, () => {
+ expect(badge.props('variant')).toBe(variant);
+ });
+ it(`sets the icon to ${icon}`, () => {
+ expect(badge.props('icon')).toBe(icon);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index db78a6b0cdd..1b68a692db8 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,9 +1,13 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { TEST_HOST } from 'helpers/test_constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
import eventHub from '~/environments/event_hub';
+import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
const scheduledJobAction = {
name: 'scheduled action',
@@ -25,12 +29,13 @@ describe('EnvironmentActions Component', () => {
const findEnvironmentActionsButton = () =>
wrapper.find('[data-testid="environment-actions-button"]');
- function createComponent(props, { mountFn = shallowMount } = {}) {
+ function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
wrapper = mountFn(EnvironmentActions, {
propsData: { actions: [], ...props },
directives: {
GlTooltip: createMockDirective(),
},
+ ...options,
});
}
@@ -150,4 +155,32 @@ describe('EnvironmentActions Component', () => {
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
+
+ describe('graphql', () => {
+ Vue.use(VueApollo);
+
+ const action = {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ };
+
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApollo();
+ createComponent(
+ { actions: [action], graphql: true },
+ { options: { apolloProvider: mockApollo } },
+ );
+ });
+
+ it('should trigger a graphql mutation on click', () => {
+ jest.spyOn(mockApollo.defaultClient, 'mutate');
+ findDropdownItem(action).vm.$emit('click');
+ expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: actionMutation,
+ variables: { action },
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index dff444b79f3..358abca2f77 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -1,38 +1,80 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import setEnvironmentToStopMutation from '~/environments/graphql/mutations/set_environment_to_stop.mutation.graphql';
+import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql';
import StopComponent from '~/environments/components/environment_stop.vue';
import eventHub from '~/environments/event_hub';
-
-$.fn.tooltip = () => {};
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { resolvedEnvironment } from './graphql/mock_data';
describe('Stop Component', () => {
let wrapper;
- const createWrapper = () => {
+ const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(StopComponent, {
propsData: {
environment: {},
+ ...props,
},
+ ...options,
});
};
const findButton = () => wrapper.find(GlButton);
- beforeEach(() => {
- jest.spyOn(window, 'confirm');
+ describe('eventHub', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
- createWrapper();
- });
+ it('should render a button to stop the environment', () => {
+ expect(findButton().exists()).toBe(true);
+ expect(wrapper.attributes('title')).toEqual('Stop environment');
+ });
- it('should render a button to stop the environment', () => {
- expect(findButton().exists()).toBe(true);
- expect(wrapper.attributes('title')).toEqual('Stop environment');
+ it('emits requestStopEnvironment in the event hub when button is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+ findButton().vm.$emit('click');
+ expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment);
+ });
});
- it('emits requestStopEnvironment in the event hub when button is clicked', () => {
- jest.spyOn(eventHub, '$emit');
- findButton().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment);
+ describe('graphql', () => {
+ Vue.use(VueApollo);
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApollo();
+ mockApollo.clients.defaultClient.writeQuery({
+ query: isEnvironmentStoppingQuery,
+ variables: { environment: resolvedEnvironment },
+ data: { isEnvironmentStopping: true },
+ });
+
+ createWrapper(
+ { graphql: true, environment: resolvedEnvironment },
+ { apolloProvider: mockApollo },
+ );
+ });
+
+ it('should render a button to stop the environment', () => {
+ expect(findButton().exists()).toBe(true);
+ expect(wrapper.attributes('title')).toEqual('Stop environment');
+ });
+
+ it('sets the environment to stop on click', () => {
+ jest.spyOn(mockApollo.defaultClient, 'mutate');
+ findButton().vm.$emit('click');
+ expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: setEnvironmentToStopMutation,
+ variables: { environment: resolvedEnvironment },
+ });
+ });
+
+ it('should show a loading icon if the environment is currently stopping', async () => {
+ expect(findButton().props('loading')).toBe(true);
+ });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index e75d3ac0321..fce30973547 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -477,7 +477,141 @@ export const resolvedEnvironment = {
externalUrl: 'https://example.org',
environmentType: 'review',
nameWithoutType: 'hello',
- lastDeployment: null,
+ lastDeployment: {
+ id: 78,
+ iid: 24,
+ sha: 'f3ba6dd84f8f891373e9b869135622b954852db1',
+ ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' },
+ status: 'success',
+ createdAt: '2022-01-07T15:47:27.415Z',
+ deployedAt: '2022-01-07T15:47:32.450Z',
+ tag: false,
+ isLast: true,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gck.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 1014,
+ name: 'deploy-prod',
+ started: '2022-01-07T15:47:31.037Z',
+ complete: true,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/1014',
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+ playable: false,
+ scheduled: false,
+ createdAt: '2022-01-07T15:47:27.404Z',
+ updatedAt: '2022-01-07T15:47:32.341Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014',
+ illustration: {
+ image:
+ '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+ method: 'post',
+ buttonTitle: 'Retry this job',
+ },
+ },
+ },
+ commit: {
+ id: 'f3ba6dd84f8f891373e9b869135622b954852db1',
+ shortId: 'f3ba6dd8',
+ createdAt: '2022-01-07T15:47:26.000+00:00',
+ parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'],
+ title: 'Update .gitlab-ci.yml file',
+ message: 'Update .gitlab-ci.yml file',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ authoredDate: '2022-01-07T15:47:26.000+00:00',
+ committerName: 'Administrator',
+ committerEmail: 'admin@example.com',
+ committedDate: '2022-01-07T15:47:26.000+00:00',
+ trailers: {},
+ webUrl:
+ 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ author: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gck.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ authorGravatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commitUrl:
+ 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ },
+ manualActions: [
+ {
+ id: 1015,
+ name: 'deploy-staging',
+ started: null,
+ complete: false,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/1015',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play',
+ playable: true,
+ scheduled: false,
+ createdAt: '2022-01-07T15:47:27.422Z',
+ updatedAt: '2022-01-07T15:47:28.557Z',
+ status: {
+ icon: 'status_manual',
+ text: 'manual',
+ label: 'manual play action',
+ group: 'manual',
+ tooltip: 'manual action',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
+ method: 'post',
+ buttonTitle: 'Trigger this manual action',
+ },
+ },
+ },
+ ],
+ scheduledActions: [],
+ cluster: null,
+ },
hasStopAction: false,
rolloutStatus: null,
environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index d8d26b74504..6b53dc24f0f 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,8 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
+import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
+import environmentToStopQuery from '~/environments/graphql/queries/environment_to_stop.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql';
import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql';
@@ -210,4 +212,36 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
});
});
+ describe('setEnvironmentToStop', () => {
+ it('should write the given environment to the cache', () => {
+ localState.client.writeQuery = jest.fn();
+ mockResolvers.Mutation.setEnvironmentToStop(
+ null,
+ { environment: resolvedEnvironment },
+ localState,
+ );
+
+ expect(localState.client.writeQuery).toHaveBeenCalledWith({
+ query: environmentToStopQuery,
+ data: { environmentToStop: resolvedEnvironment },
+ });
+ });
+ });
+ describe('action', () => {
+ it('should POST to the given path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+ const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
+
+ expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
+ });
+ it('should return a nice error message on fail', async () => {
+ mock.onPost(ENDPOINT).reply(500);
+ const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
+
+ expect(errors).toEqual({
+ __typename: 'LocalEnvironmentErrors',
+ errors: [s__('Environments|An error occurred while making the request.')],
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js
index 27d27d5869a..6823c88a5a1 100644
--- a/spec/frontend/environments/new_environment_folder_spec.js
+++ b/spec/frontend/environments/new_environment_folder_spec.js
@@ -1,10 +1,13 @@
import VueApollo from 'vue-apollo';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { GlCollapse, GlIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubTransition } from 'helpers/stub_transition';
import { __, s__ } from '~/locale';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
+import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -25,13 +28,20 @@ describe('~/environments/components/new_environments_folder.vue', () => {
};
const createWrapper = (propsData, apolloProvider) =>
- mountExtended(EnvironmentsFolder, { apolloProvider, propsData });
+ mountExtended(EnvironmentsFolder, {
+ apolloProvider,
+ propsData,
+ stubs: { transition: stubTransition() },
+ });
- beforeEach(() => {
+ beforeEach(async () => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
+
+ await nextTick();
+ await waitForPromises();
folderName = wrapper.findByText(nestedEnvironment.name);
button = wrapper.findByRole('button', { name: __('Expand') });
});
@@ -57,7 +67,8 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const link = findLink();
expect(collapse.attributes('visible')).toBeUndefined();
- expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']);
+ const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
+ expect(iconNames).toEqual(['angle-right', 'folder-o']);
expect(folderName.classes('gl-font-weight-bold')).toBe(false);
expect(link.exists()).toBe(false);
});
@@ -68,10 +79,21 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const link = findLink();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
- expect(collapse.attributes('visible')).toBe('true');
- expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']);
+ expect(collapse.attributes('visible')).toBe('visible');
+ const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
+ expect(iconNames).toEqual(['angle-down', 'folder-open']);
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
});
+
+ it('displays all environments when opened', async () => {
+ await button.trigger('click');
+
+ const names = resolvedFolder.environments.map((e) =>
+ expect.stringMatching(e.nameWithoutType),
+ );
+ const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text());
+ expect(environments).toEqual(expect.arrayContaining(names));
+ });
});
});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
new file mode 100644
index 00000000000..244aef5c43b
--- /dev/null
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -0,0 +1,341 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlCollapse, GlIcon } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubTransition } from 'helpers/stub_transition';
+import { __, s__ } from '~/locale';
+import EnvironmentItem from '~/environments/components/new_environment_item.vue';
+import Deployment from '~/environments/components/deployment.vue';
+import { resolvedEnvironment } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/new_environment_item.vue', () => {
+ let wrapper;
+
+ const createApolloProvider = () => {
+ return createMockApollo();
+ };
+
+ const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
+ mountExtended(EnvironmentItem, {
+ apolloProvider,
+ propsData: { environment: resolvedEnvironment, ...propsData },
+ stubs: { transition: stubTransition() },
+ });
+
+ const findDeployment = () => wrapper.findComponent(Deployment);
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('displays the name when not in a folder', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const name = wrapper.findByRole('link', { name: resolvedEnvironment.name });
+ expect(name.exists()).toBe(true);
+ });
+
+ it('displays the name minus the folder prefix when in a folder', () => {
+ wrapper = createWrapper({
+ propsData: { inFolder: true },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const name = wrapper.findByRole('link', { name: resolvedEnvironment.nameWithoutType });
+ expect(name.exists()).toBe(true);
+ });
+
+ it('truncates the name if it is very long', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ name:
+ 'this is a really long name that should be truncated because otherwise it would look strange in the UI',
+ };
+ wrapper = createWrapper({ propsData: { environment }, apolloProvider: createApolloProvider() });
+
+ const name = wrapper.findByRole('link', {
+ name: (text) => environment.name.startsWith(text.slice(0, -1)),
+ });
+ expect(name.exists()).toBe(true);
+ expect(name.text()).toHaveLength(80);
+ });
+
+ describe('url', () => {
+ it('shows a link for the url if one is present', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
+
+ expect(url.attributes('href')).toEqual(resolvedEnvironment.externalUrl);
+ });
+
+ it('does not show a link for the url if one is missing', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, externalUrl: '' } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
+
+ expect(url.exists()).toBe(false);
+ });
+ });
+
+ describe('actions', () => {
+ it('shows a dropdown if there are actions to perform', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
+
+ expect(actions.exists()).toBe(true);
+ });
+
+ it('does not show a dropdown if there are no actions to perform', () => {
+ wrapper = createWrapper({
+ propsData: {
+ environment: {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ },
+ apolloProvider: createApolloProvider(),
+ },
+ });
+
+ const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
+
+ expect(actions.exists()).toBe(false);
+ });
+
+ it('passes all the actions down to the action component', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
+
+ expect(action.exists()).toBe(true);
+ });
+ });
+
+ describe('stop', () => {
+ it('shows a buton to stop the environment if the environment is available', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
+
+ expect(stop.exists()).toBe(true);
+ });
+
+ it('does not show a buton to stop the environment if the environment is stopped', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, canStop: false } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
+
+ expect(stop.exists()).toBe(false);
+ });
+ });
+
+ describe('rollback', () => {
+ it('shows the option to rollback/re-deploy if available', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Re-deploy to environment'),
+ });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the option to rollback/re-deploy if not available', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, lastDeployment: null } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Re-deploy to environment'),
+ });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('pin', () => {
+ it('shows the option to pin the environment if there is an autostop date', () => {
+ wrapper = createWrapper({
+ propsData: {
+ environment: { ...resolvedEnvironment, autoStopAt: new Date(Date.now() + 100000) },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the option to pin the environment if there is no autostop date', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('monitoring', () => {
+ it('shows the link to monitoring if metrics are set up', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the link to monitoring if metrics are not set up', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+ describe('terminal', () => {
+ it('shows the link to the terminal if set up', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, terminalPath: '/terminal' } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the link to the terminal if not set up', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('delete', () => {
+ it('shows the button to delete the environment if possible', () => {
+ wrapper = createWrapper({
+ propsData: {
+ environment: { ...resolvedEnvironment, canDelete: true, deletePath: '/terminal' },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Delete environment'),
+ });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the button to delete the environment if not possible', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Delete environment'),
+ });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('collapse', () => {
+ let icon;
+ let collapse;
+ let button;
+ let environmentName;
+
+ beforeEach(() => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+ collapse = wrapper.findComponent(GlCollapse);
+ icon = wrapper.findComponent(GlIcon);
+ button = wrapper.findByRole('button', { name: __('Expand') });
+ environmentName = wrapper.findByText(resolvedEnvironment.name);
+ });
+
+ it('is collapsed by default', () => {
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(icon.props('name')).toEqual('angle-right');
+ expect(environmentName.classes('gl-font-weight-bold')).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ expect(findDeployment().isVisible()).toBe(false);
+
+ await button.trigger('click');
+
+ expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(collapse.attributes('visible')).toBe('visible');
+ expect(icon.props('name')).toEqual('angle-down');
+ expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
+ expect(findDeployment().isVisible()).toBe(true);
+ });
+ });
+ describe('last deployment', () => {
+ it('should pass the last deployment to the deployment component when it exists', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const deployment = findDeployment();
+ expect(deployment.props('deployment')).toEqual(resolvedEnvironment.lastDeployment);
+ });
+ it('should not show the last deployment when it is missing', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const deployment = findDeployment();
+ expect(deployment.exists()).toBe(false);
+ });
+ });
+
+ describe('upcoming deployment', () => {
+ it('should pass the upcoming deployment to the deployment component when it exists', () => {
+ const upcomingDeployment = resolvedEnvironment.lastDeployment;
+ const environment = { ...resolvedEnvironment, lastDeployment: null, upcomingDeployment };
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const deployment = findDeployment();
+ expect(deployment.props('deployment')).toEqual(upcomingDeployment);
+ });
+ it('should not show the upcoming deployment when it is missing', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ upcomingDeployment: null,
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const deployment = findDeployment();
+ expect(deployment.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js
index 1e9bd4d64c9..c9eccc26694 100644
--- a/spec/frontend/environments/new_environments_app_spec.js
+++ b/spec/frontend/environments/new_environments_app_spec.js
@@ -8,7 +8,9 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { sprintf, __, s__ } from '~/locale';
import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
-import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
+import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
+import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
+import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -17,6 +19,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
let environmentAppMock;
let environmentFolderMock;
let paginationMock;
+ let environmentToStopMock;
const createApolloProvider = () => {
const mockResolvers = {
@@ -24,6 +27,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentApp: environmentAppMock,
folder: environmentFolderMock,
pageInfo: paginationMock,
+ environmentToStop: environmentToStopMock,
},
};
@@ -45,6 +49,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
provide = {},
environmentsApp,
folder,
+ environmentToStop = {},
pageInfo = {
total: 20,
perPage: 5,
@@ -58,6 +63,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentAppMock.mockReturnValue(environmentsApp);
environmentFolderMock.mockReturnValue(folder);
paginationMock.mockReturnValue(pageInfo);
+ environmentToStopMock.mockReturnValue(environmentToStop);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider, provide });
@@ -68,6 +74,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
beforeEach(() => {
environmentAppMock = jest.fn();
environmentFolderMock = jest.fn();
+ environmentToStopMock = jest.fn();
paginationMock = jest.fn();
});
@@ -87,6 +94,18 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(text).not.toContainEqual(expect.stringMatching('production'));
});
+ it('should show all the environments that are fetched', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: resolvedEnvironmentsApp,
+ folder: resolvedFolder,
+ });
+
+ const text = wrapper.findAllComponents(EnvironmentsItem).wrappers.map((w) => w.text());
+
+ expect(text).not.toContainEqual(expect.stringMatching('review'));
+ expect(text).toContainEqual(expect.stringMatching('production'));
+ });
+
it('should show a button to create a new environment', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
@@ -168,13 +187,27 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(environmentAppMock).toHaveBeenCalledWith(
expect.anything(),
- expect.objectContaining({ scope: 'stopped' }),
+ expect.objectContaining({ scope: 'stopped', page: 1 }),
expect.anything(),
expect.anything(),
);
});
});
+ describe('modals', () => {
+ it('should pass the environment to stop to the stop environment modal', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: resolvedEnvironmentsApp,
+ folder: resolvedFolder,
+ environmentToStop: resolvedEnvironment,
+ });
+
+ const modal = wrapper.findComponent(StopEnvironmentModal);
+
+ expect(modal.props('environment')).toMatchObject(resolvedEnvironment);
+ });
+ });
+
describe('pagination', () => {
it('should sync page from query params on load', async () => {
await createWrapperWithMocked({
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 4e459d800e8..77f51193258 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -173,6 +173,8 @@ describe('ErrorDetails', () => {
beforeEach(() => {
mocks.$apollo.queries.error.loading = false;
mountComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
id: 'gid://gitlab/Gitlab::ErrorTracking::DetailedError/129381',
@@ -203,6 +205,8 @@ describe('ErrorDetails', () => {
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
culprit,
@@ -222,6 +226,8 @@ describe('ErrorDetails', () => {
describe('Badges', () => {
it('should show language and error level badges', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: 'error', logger: 'ruby' },
@@ -233,6 +239,8 @@ describe('ErrorDetails', () => {
});
it('should NOT show the badge if the tag is not present', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: 'error' },
@@ -246,6 +254,8 @@ describe('ErrorDetails', () => {
it.each(Object.keys(severityLevel))(
'should set correct severity level variant for %s badge',
(level) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: severityLevel[level] },
@@ -260,6 +270,8 @@ describe('ErrorDetails', () => {
);
it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: 'someNewErrorLevel' },
@@ -408,6 +420,8 @@ describe('ErrorDetails', () => {
it('should show alert with closed issueId', () => {
const closedIssueId = 123;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isAlertVisible: true,
closedIssueId,
@@ -429,6 +443,8 @@ describe('ErrorDetails', () => {
describe('is present', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabIssuePath,
@@ -451,6 +467,8 @@ describe('ErrorDetails', () => {
describe('is not present', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabIssuePath: null,
@@ -480,6 +498,8 @@ describe('ErrorDetails', () => {
it('should display a link', () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabCommit,
@@ -493,6 +513,8 @@ describe('ErrorDetails', () => {
it('should not display a link', () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabCommit: null,
@@ -519,6 +541,8 @@ describe('ErrorDetails', () => {
it('should display links to Sentry', async () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
error: {
firstReleaseVersion,
@@ -535,6 +559,8 @@ describe('ErrorDetails', () => {
it('should display links to GitLab when integrated', async () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
error: {
firstReleaseVersion,
@@ -557,6 +583,8 @@ describe('ErrorDetails', () => {
jest.spyOn(Tracking, 'event');
mocks.$apollo.queries.error.loading = false;
mountComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: { externalUrl },
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index c0c542ae587..74d5731bbea 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -396,6 +396,8 @@ describe('ErrorTrackingList', () => {
GlPagination: false,
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ pageValue: 2 });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index bfdeee0881b..35a7ff4eb07 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -12,6 +12,7 @@ RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :control
render_views
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This fixture is only used by the legacy (non-refactored) blob viewer
sign_in(user)
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index fa150fbf57c..36e6cf72750 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -24,80 +24,109 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
- describe GraphQL::Query, type: :request do
- get_runners_query_name = 'get_runners.query.graphql'
-
+ describe do
before do
sign_in(admin)
enable_admin_mode!(admin)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_runners_query_name = 'get_runners.query.graphql'
- it "#{fixtures_path}#{get_runners_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {})
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
+ end
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{get_runners_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: admin, variables: { first: 2 })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: admin, variables: { first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
end
- end
- describe GraphQL::Query, type: :request do
- get_runner_query_name = 'get_runner.query.graphql'
+ describe GraphQL::Query, type: :request do
+ get_runners_count_query_name = 'get_runners_count.query.graphql'
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runners_count_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_runner_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
- })
+ describe GraphQL::Query, type: :request do
+ get_runner_query_name = 'get_runner.query.graphql'
- expect_graphql_errors_to_be_empty
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runner_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
end
- describe GraphQL::Query, type: :request do
- get_group_runners_query_name = 'get_group_runners.query.graphql'
-
+ describe do
let_it_be(:group_owner) { create(:user) }
before do
group.add_owner(group_owner)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_group_runners_query_name = 'get_group_runners.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
+ end
- it "#{fixtures_path}#{get_group_runners_query_name}.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path
- })
+ it "#{fixtures_path}#{get_group_runners_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path,
+ first: 1
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path,
- first: 1
- })
+ describe GraphQL::Query, type: :request do
+ get_group_runners_count_query_name = 'get_group_runners_count.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_count_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_count_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
end
diff --git a/spec/frontend/fixtures/static/project_select_combo_button.html b/spec/frontend/fixtures/static/project_select_combo_button.html
index 444e0bc84a2..3776610ed4c 100644
--- a/spec/frontend/fixtures/static/project_select_combo_button.html
+++ b/spec/frontend/fixtures/static/project_select_combo_button.html
@@ -1,6 +1,6 @@
<div class="project-item-select-holder">
<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new" />
- <a class="new-project-item-link" data-label="New issue" data-type="issues" href="">
+ <a class="js-new-project-item-link" data-label="issue" data-type="issues" href="">
<span class="gl-spinner"></span>
</a>
<a class="new-project-item-select-button">
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index fc736f2d155..d5451ec2064 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,9 +1,12 @@
import * as Sentry from '@sentry/browser';
+import { setHTMLFixture } from 'helpers/fixtures';
import createFlash, {
hideFlash,
addDismissFlashClickListener,
FLASH_TYPES,
FLASH_CLOSED_EVENT,
+ createAlert,
+ VARIANT_WARNING,
} from '~/flash';
jest.mock('@sentry/browser');
@@ -68,6 +71,236 @@ describe('Flash', () => {
});
});
+ describe('createAlert', () => {
+ const mockMessage = 'a message';
+ let alert;
+
+ describe('no flash-container', () => {
+ it('does not add to the DOM', () => {
+ alert = createAlert({ message: mockMessage });
+
+ expect(alert).toBeNull();
+ expect(document.querySelector('.gl-alert')).toBeNull();
+ });
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ setHTMLFixture('<div class="flash-container"></div>');
+ });
+
+ afterEach(() => {
+ if (alert) {
+ alert.$destroy();
+ }
+ document.querySelector('.flash-container')?.remove();
+ });
+
+ it('adds alert element into the document by default', () => {
+ alert = createAlert({ message: mockMessage });
+
+ expect(document.querySelector('.flash-container').textContent.trim()).toBe(mockMessage);
+ expect(document.querySelector('.flash-container .gl-alert')).not.toBeNull();
+ });
+
+ it('adds flash of a warning type', () => {
+ alert = createAlert({ message: mockMessage, variant: VARIANT_WARNING });
+
+ expect(
+ document.querySelector('.flash-container .gl-alert.gl-alert-warning'),
+ ).not.toBeNull();
+ });
+
+ it('escapes text', () => {
+ alert = createAlert({ message: '<script>alert("a");</script>' });
+
+ const html = document.querySelector('.flash-container').innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a");&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a");</script>');
+ });
+
+ it('adds alert into specified container', () => {
+ setHTMLFixture(`
+ <div class="my-alert-container"></div>
+ <div class="my-other-container"></div>
+ `);
+
+ alert = createAlert({ message: mockMessage, containerSelector: '.my-alert-container' });
+
+ expect(document.querySelector('.my-alert-container .gl-alert')).not.toBeNull();
+ expect(document.querySelector('.my-alert-container').innerText.trim()).toBe(mockMessage);
+
+ expect(document.querySelector('.my-other-container .gl-alert')).toBeNull();
+ expect(document.querySelector('.my-other-container').innerText.trim()).toBe('');
+ });
+
+ it('adds alert into specified parent', () => {
+ setHTMLFixture(`
+ <div id="my-parent">
+ <div class="flash-container"></div>
+ </div>
+ <div id="my-other-parent">
+ <div class="flash-container"></div>
+ </div>
+ `);
+
+ alert = createAlert({ message: mockMessage, parent: document.getElementById('my-parent') });
+
+ expect(document.querySelector('#my-parent .flash-container .gl-alert')).not.toBeNull();
+ expect(document.querySelector('#my-parent .flash-container').innerText.trim()).toBe(
+ mockMessage,
+ );
+
+ expect(document.querySelector('#my-other-parent .flash-container .gl-alert')).toBeNull();
+ expect(document.querySelector('#my-other-parent .flash-container').innerText.trim()).toBe(
+ '',
+ );
+ });
+
+ it('removes element after clicking', () => {
+ alert = createAlert({ message: mockMessage });
+
+ expect(document.querySelector('.flash-container .gl-alert')).not.toBeNull();
+
+ document.querySelector('.gl-dismiss-btn').click();
+
+ expect(document.querySelector('.flash-container .gl-alert')).toBeNull();
+ });
+
+ it('does not capture error using Sentry', () => {
+ alert = createAlert({
+ message: mockMessage,
+ captureError: false,
+ error: new Error('Error!'),
+ });
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('captures error using Sentry', () => {
+ alert = createAlert({
+ message: mockMessage,
+ captureError: true,
+ error: new Error('Error!'),
+ });
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
+ expect(Sentry.captureException).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Error!',
+ }),
+ );
+ });
+
+ describe('with buttons', () => {
+ const findAlertAction = () => document.querySelector('.flash-container .gl-alert-action');
+
+ it('adds primary button', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: 'Ok',
+ },
+ });
+
+ expect(findAlertAction().textContent.trim()).toBe('Ok');
+ });
+
+ it('creates link with href', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ link: '/url',
+ text: 'Ok',
+ },
+ });
+
+ const action = findAlertAction();
+
+ expect(action.textContent.trim()).toBe('Ok');
+ expect(action.nodeName).toBe('A');
+ expect(action.getAttribute('href')).toBe('/url');
+ });
+
+ it('create button as href when no href is present', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: 'Ok',
+ },
+ });
+
+ const action = findAlertAction();
+
+ expect(action.nodeName).toBe('BUTTON');
+ expect(action.getAttribute('href')).toBe(null);
+ });
+
+ it('escapes the title text', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: '<script>alert("a")</script>',
+ },
+ });
+
+ const html = findAlertAction().innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a")</script>');
+ });
+
+ it('calls actionConfig clickHandler on click', () => {
+ const clickHandler = jest.fn();
+
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: 'Ok',
+ clickHandler,
+ },
+ });
+
+ expect(clickHandler).toHaveBeenCalledTimes(0);
+
+ findAlertAction().click();
+
+ expect(clickHandler).toHaveBeenCalledTimes(1);
+ expect(clickHandler).toHaveBeenCalledWith(expect.any(MouseEvent));
+ });
+ });
+
+ describe('Alert API', () => {
+ describe('dismiss', () => {
+ it('dismiss programmatically with .dismiss()', () => {
+ expect(document.querySelector('.gl-alert')).toBeNull();
+
+ alert = createAlert({ message: mockMessage });
+
+ expect(document.querySelector('.gl-alert')).not.toBeNull();
+
+ alert.dismiss();
+
+ expect(document.querySelector('.gl-alert')).toBeNull();
+ });
+
+ it('calls onDismiss when dismissed', () => {
+ const dismissHandler = jest.fn();
+
+ alert = createAlert({ message: mockMessage, onDismiss: dismissHandler });
+
+ expect(dismissHandler).toHaveBeenCalledTimes(0);
+
+ alert.dismiss();
+
+ expect(dismissHandler).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+ });
+ });
+
describe('createFlash', () => {
const message = 'test';
const fadeTransition = false;
@@ -91,7 +324,7 @@ describe('Flash', () => {
describe('with flash-container', () => {
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div class="content-wrapper js-content-wrapper"><div class="flash-container"></div></div>',
);
});
@@ -115,11 +348,12 @@ describe('Flash', () => {
});
it('escapes text', () => {
- createFlash({ ...defaultParams, message: '<script>alert("a");</script>' });
+ createFlash({ ...defaultParams, message: '<script>alert("a")</script>' });
- expect(document.querySelector('.flash-text').textContent.trim()).toBe(
- '<script>alert("a");</script>',
- );
+ const html = document.querySelector('.flash-text').innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a")</script>');
});
it('adds flash into specified parent', () => {
@@ -193,8 +427,10 @@ describe('Flash', () => {
},
});
- expect(findFlashAction().href).toBe(`${window.location}testing`);
- expect(findFlashAction().textContent.trim()).toBe('test');
+ const action = findFlashAction();
+
+ expect(action.href).toBe(`${window.location}testing`);
+ expect(action.textContent.trim()).toBe('test');
});
it('uses hash as href when no href is present', () => {
@@ -227,7 +463,10 @@ describe('Flash', () => {
},
});
- expect(findFlashAction().textContent.trim()).toBe('<script>alert("a")</script>');
+ const html = findFlashAction().innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a")</script>');
});
it('calls actionConfig clickHandler on click', () => {
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 570ac1e6ed1..92bc7596f7d 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -24,6 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
describe('google_cloud App component', () => {
diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
new file mode 100644
index 00000000000..76c3bfd00a8
--- /dev/null
+++ b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
@@ -0,0 +1,40 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlTable } from '@gitlab/ui';
+import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue';
+
+describe('google_cloud DeploymentsServiceTable component', () => {
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findButtons = () => findTable().findAllComponents(GlButton);
+ const findCloudRunButton = () => findButtons().at(0);
+ const findCloudStorageButton = () => findButtons().at(1);
+
+ beforeEach(() => {
+ const propsData = {
+ cloudRunUrl: '#url-deployments-cloud-run',
+ cloudStorageUrl: '#url-deployments-cloud-storage',
+ };
+ wrapper = mount(DeploymentsServiceTable, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should contain a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('should contain configure cloud run button', () => {
+ const cloudRunButton = findCloudRunButton();
+ expect(cloudRunButton.exists()).toBe(true);
+ expect(cloudRunButton.props().disabled).toBe(true);
+ });
+
+ it('should contain configure cloud storage button', () => {
+ const cloudStorageButton = findCloudStorageButton();
+ expect(cloudStorageButton.exists()).toBe(true);
+ expect(cloudStorageButton.props().disabled).toBe(true);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js
index 9b4c3a79f11..3a009fc88ce 100644
--- a/spec/frontend/google_cloud/components/home_spec.js
+++ b/spec/frontend/google_cloud/components/home_spec.js
@@ -20,6 +20,8 @@ describe('google_cloud Home component', () => {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
beforeEach(() => {
@@ -42,7 +44,7 @@ describe('google_cloud Home component', () => {
it('should contain three tab items', () => {
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
- { title: 'Deployments', disabled: '' },
+ { title: 'Deployments', disabled: undefined },
{ title: 'Services', disabled: '' },
]);
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
new file mode 100644
index 00000000000..ff38de28da6
--- /dev/null
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -0,0 +1,259 @@
+import { merge } from 'lodash';
+import {
+ trackFreeTrialAccountSubmissions,
+ trackNewRegistrations,
+ trackSaasTrialSubmit,
+ trackSaasTrialSkip,
+ trackSaasTrialGroup,
+ trackSaasTrialProject,
+ trackSaasTrialProjectImport,
+ trackSaasTrialGetStarted,
+} from '~/google_tag_manager';
+import { setHTMLFixture } from 'helpers/fixtures';
+import { logError } from '~/lib/logger';
+
+jest.mock('~/lib/logger');
+
+describe('~/google_tag_manager/index', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.fn();
+
+ window.dataLayer = {
+ push: spy,
+ };
+ window.gon.features = {
+ gitlabGtmDatalayer: true,
+ };
+ });
+
+ const createHTML = ({ links = [], forms = [] } = {}) => {
+ // .foo elements are used to test elements which shouldn't do anything
+ const allLinks = links.concat({ cls: 'foo' });
+ const allForms = forms.concat({ cls: 'foo' });
+
+ const el = document.createElement('div');
+
+ allLinks.forEach(({ cls = '', id = '', href = '#', text = 'Hello', attributes = {} }) => {
+ const a = document.createElement('a');
+ a.id = id;
+ a.href = href || '#';
+ a.className = cls;
+ a.textContent = text;
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ a.setAttribute(key, value);
+ });
+
+ el.append(a);
+ });
+
+ allForms.forEach(({ cls = '', id = '' }) => {
+ const form = document.createElement('form');
+ form.id = id;
+ form.className = cls;
+
+ el.append(form);
+ });
+
+ return el.innerHTML;
+ };
+
+ const triggerEvent = (selector, eventType) => {
+ const el = document.querySelector(selector);
+
+ el.dispatchEvent(new Event(eventType));
+ };
+
+ const getSelector = ({ id, cls }) => (id ? `#${id}` : `.${cls}`);
+
+ const createTestCase = (subject, { forms = [], links = [] }) => {
+ const expectedFormEvents = forms.map(({ expectation, ...form }) => ({
+ selector: getSelector(form),
+ trigger: 'submit',
+ expectation,
+ }));
+
+ const expectedLinkEvents = links.map(({ expectation, ...link }) => ({
+ selector: getSelector(link),
+ trigger: 'click',
+ expectation,
+ }));
+
+ return [
+ subject,
+ {
+ forms,
+ links,
+ expectedEvents: [...expectedFormEvents, ...expectedLinkEvents],
+ },
+ ];
+ };
+
+ const createOmniAuthTestCase = (subject, accountType) =>
+ createTestCase(subject, {
+ forms: [
+ {
+ id: 'new_new_user',
+ expectation: {
+ event: 'accountSubmit',
+ accountMethod: 'form',
+ accountType,
+ },
+ },
+ ],
+ links: [
+ {
+ // id is needed so that the test selects the right element to trigger
+ id: 'test-0',
+ cls: 'js-oauth-login',
+ attributes: {
+ 'data-provider': 'myspace',
+ },
+ expectation: {
+ event: 'accountSubmit',
+ accountMethod: 'myspace',
+ accountType,
+ },
+ },
+ {
+ id: 'test-1',
+ cls: 'js-oauth-login',
+ attributes: {
+ 'data-provider': 'gitlab',
+ },
+ expectation: {
+ event: 'accountSubmit',
+ accountMethod: 'gitlab',
+ accountType,
+ },
+ },
+ ],
+ });
+
+ describe.each([
+ createOmniAuthTestCase(trackFreeTrialAccountSubmissions, 'freeThirtyDayTrial'),
+ createOmniAuthTestCase(trackNewRegistrations, 'standardSignUp'),
+ createTestCase(trackSaasTrialSkip, {
+ links: [{ cls: 'js-skip-trial', expectation: { event: 'saasTrialSkip' } }],
+ }),
+ createTestCase(trackSaasTrialGroup, {
+ forms: [{ cls: 'js-saas-trial-group', expectation: { event: 'saasTrialGroup' } }],
+ }),
+ createTestCase(trackSaasTrialProject, {
+ forms: [{ id: 'new_project', expectation: { event: 'saasTrialProject' } }],
+ }),
+ createTestCase(trackSaasTrialProjectImport, {
+ links: [
+ {
+ id: 'js-test-btn-0',
+ cls: 'js-import-project-btn',
+ attributes: { 'data-platform': 'bitbucket' },
+ expectation: { event: 'saasTrialProjectImport', saasProjectImport: 'bitbucket' },
+ },
+ {
+ // id is neeeded so we trigger the right element in the test
+ id: 'js-test-btn-1',
+ cls: 'js-import-project-btn',
+ attributes: { 'data-platform': 'github' },
+ expectation: { event: 'saasTrialProjectImport', saasProjectImport: 'github' },
+ },
+ ],
+ }),
+ createTestCase(trackSaasTrialGetStarted, {
+ links: [
+ {
+ cls: 'js-get-started-btn',
+ expectation: { event: 'saasTrialGetStarted' },
+ },
+ ],
+ }),
+ ])('%p', (subject, { links = [], forms = [], expectedEvents }) => {
+ beforeEach(() => {
+ setHTMLFixture(createHTML({ links, forms }));
+
+ subject();
+ });
+
+ it.each(expectedEvents)('when %p', ({ selector, trigger, expectation }) => {
+ expect(spy).not.toHaveBeenCalled();
+
+ triggerEvent(selector, trigger);
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith(expectation);
+ expect(logError).not.toHaveBeenCalled();
+ });
+
+ it('when random link is clicked, does nothing', () => {
+ triggerEvent('a.foo', 'click');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('when random form is submitted, does nothing', () => {
+ triggerEvent('form.foo', 'submit');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('No listener events', () => {
+ it('when trackSaasTrialSubmit is invoked', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackSaasTrialSubmit();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith({ event: 'saasTrialSubmit' });
+ expect(logError).not.toHaveBeenCalled();
+ });
+ });
+
+ describe.each([
+ { dataLayer: null },
+ { gon: { features: null } },
+ { gon: { features: { gitlabGtmDatalayer: false } } },
+ ])('when window %o', (windowAttrs) => {
+ beforeEach(() => {
+ merge(window, windowAttrs);
+ });
+
+ it('no ops', () => {
+ setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] }));
+
+ trackSaasTrialProject();
+
+ triggerEvent('#new_project', 'submit');
+
+ expect(spy).not.toHaveBeenCalled();
+ expect(logError).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when window.dataLayer throws error', () => {
+ const pushError = new Error('test');
+
+ beforeEach(() => {
+ window.dataLayer = {
+ push() {
+ throw pushError;
+ },
+ };
+ });
+
+ it('logs error', () => {
+ setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] }));
+
+ trackSaasTrialProject();
+
+ triggerEvent('#new_project', 'submit');
+
+ expect(logError).toHaveBeenCalledWith(
+ 'Unexpected error while pushing to dataLayer',
+ pushError,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 60d47895a95..8ea7e54aef4 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -100,6 +100,7 @@ describe('GroupItemComponent', () => {
wrapper.destroy();
group.type = 'project';
+ group.lastActivityAt = '2017-04-09T18:40:39.101Z';
wrapper = createComponent({ group });
expect(wrapper.vm.isGroup).toBe(false);
diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js
index 49f3f5da43c..fdc267bc14a 100644
--- a/spec/frontend/groups/components/item_stats_spec.js
+++ b/spec/frontend/groups/components/item_stats_spec.js
@@ -38,6 +38,7 @@ describe('ItemStats', () => {
...mockParentGroupItem,
type: ITEM_TYPE.PROJECT,
starCount: 4,
+ lastActivityAt: '2017-04-09T18:40:39.101Z',
};
createComponent({ item });
diff --git a/spec/frontend/landing_spec.js b/spec/frontend/groups/landing_spec.js
index 448d8ee2e81..f90f541eb96 100644
--- a/spec/frontend/landing_spec.js
+++ b/spec/frontend/groups/landing_spec.js
@@ -1,5 +1,5 @@
import Cookies from 'js-cookie';
-import Landing from '~/landing';
+import Landing from '~/groups/landing';
describe('Landing', () => {
const test = {};
diff --git a/spec/frontend/transfer_edit_spec.js b/spec/frontend/groups/transfer_edit_spec.js
index 4091d753fe5..bc070920d02 100644
--- a/spec/frontend/transfer_edit_spec.js
+++ b/spec/frontend/groups/transfer_edit_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { loadHTMLFixture } from 'helpers/fixtures';
-import setupTransferEdit from '~/transfer_edit';
+import setupTransferEdit from '~/groups/transfer_edit';
describe('setupTransferEdit', () => {
const formSelector = '.js-group-transfer-form';
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index faa70982fac..d1cf9f2e248 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -25,11 +25,12 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
<div
class="gl-mr-3 gl-ml-2"
>
- <span
- class="badge badge-pill"
+ <gl-badge-stub
+ size="md"
+ variant="muted"
>
- 4
- </span>
+ 4
+ </gl-badge-stub>
</div>
<gl-icon-stub
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 1768f01f3b8..b168eec0f16 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -73,6 +73,8 @@ describe('IDE clientside preview', () => {
const createInitializedComponent = () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
sandpackReady: true,
manager: {
@@ -202,6 +204,8 @@ describe('IDE clientside preview', () => {
it('returns false if loading and mainEntry exists', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
expect(wrapper.vm.showPreview).toBe(false);
@@ -209,6 +213,8 @@ describe('IDE clientside preview', () => {
it('returns true if not loading and mainEntry exists', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
expect(wrapper.vm.showPreview).toBe(true);
@@ -218,12 +224,16 @@ describe('IDE clientside preview', () => {
describe('showEmptyState', () => {
it('returns true if no mainEntry exists', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
expect(wrapper.vm.showEmptyState).toBe(true);
});
it('returns false if loading', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
expect(wrapper.vm.showEmptyState).toBe(false);
@@ -231,6 +241,8 @@ describe('IDE clientside preview', () => {
it('returns false if not loading and mainEntry exists', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
expect(wrapper.vm.showEmptyState).toBe(false);
@@ -307,6 +319,8 @@ describe('IDE clientside preview', () => {
describe('update', () => {
it('initializes manager if manager is empty', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ sandpackReady: true });
wrapper.vm.update();
@@ -340,6 +354,8 @@ describe('IDE clientside preview', () => {
describe('template', () => {
it('renders ide-preview element when showPreview is true', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
return wrapper.vm.$nextTick(() => {
@@ -349,6 +365,8 @@ describe('IDE clientside preview', () => {
it('renders empty state', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
return wrapper.vm.$nextTick(() => {
@@ -360,6 +378,8 @@ describe('IDE clientside preview', () => {
it('renders loading icon', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
return wrapper.vm.$nextTick(() => {
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index c957c64aa10..15af2d03704 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -5,7 +5,6 @@ import Vue from 'vue';
import Vuex from 'vuex';
import '~/behaviors/markdown/render_gfm';
import waitForPromises from 'helpers/wait_for_promises';
-import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
@@ -540,7 +539,6 @@ describe('RepoEditor', () => {
},
});
await vm.$nextTick();
- await vm.$nextTick();
expect(vm.initEditor).toHaveBeenCalled();
});
@@ -567,8 +565,8 @@ describe('RepoEditor', () => {
// switching from edit to diff mode usually triggers editor initialization
vm.$store.state.viewer = viewerTypes.diff;
- // we delay returning the file to make sure editor doesn't initialize before we fetch file content
- await waitUsingRealTimer(30);
+ jest.runOnlyPendingTimers();
+
return 'rawFileData123\n';
});
@@ -598,8 +596,9 @@ describe('RepoEditor', () => {
return aContent;
})
.mockImplementationOnce(async () => {
- // we delay returning fileB content to make sure the editor doesn't initialize prematurely
- await waitUsingRealTimer(30);
+ // we delay returning fileB content
+ // to make sure the editor doesn't initialize prematurely
+ jest.advanceTimersByTime(30);
return bContent;
});
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index c4b186c004a..afc49e22c83 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -128,6 +128,8 @@ describe('IDE Terminal', () => {
canScrollDown: false,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ canScrollUp: true, canScrollDown: true });
return nextTick().then(() => {
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index 9aa31136c89..3ede37e2eed 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -188,6 +188,24 @@ describe('IDE pipelines actions', () => {
.catch(done.fail);
});
});
+
+ it('sets latest pipeline to `null` and stops polling on empty project', (done) => {
+ mockedState = {
+ ...mockedState,
+ rootGetters: {
+ lastCommit: null,
+ },
+ };
+
+ testAction(
+ fetchLatestPipeline,
+ {},
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }],
+ [{ type: 'stopPipelinePolling' }],
+ done,
+ );
+ });
});
describe('requestJobs', () => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index bf044e388ea..b0fb94d2b29 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -61,7 +61,7 @@ describe('DynamicField', () => {
});
it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
- expect(findGlFormCheckbox().text()).toBe(checkboxLabel ?? defaultProps.title);
+ expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? defaultProps.title);
});
it('does not render other types of input', () => {
@@ -182,6 +182,17 @@ describe('DynamicField', () => {
expect(findGlFormGroup().find('small').text()).toBe(defaultProps.help);
});
+ describe('when type is checkbox', () => {
+ it('renders description with help text', () => {
+ createComponent({
+ type: 'checkbox',
+ });
+
+ expect(findGlFormGroup().find('small').exists()).toBe(false);
+ expect(findGlFormCheckbox().text()).toContain(defaultProps.help);
+ });
+ });
+
it('renders description with help text as HTML', () => {
const helpHTML = 'The <strong>URL</strong> of the project';
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 4c1394f3a87..8cf8a403e5d 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,9 +1,10 @@
+import { GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
import { setHTMLFixture } from 'helpers/fixtures';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
@@ -13,7 +14,6 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
-import waitForPromises from 'helpers/wait_for_promises';
import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
@@ -23,9 +23,12 @@ import {
import { createStore } from '~/integrations/edit/store';
import eventHub from '~/integrations/edit/event_hub';
import httpStatus from '~/lib/utils/http_status';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import { mockIntegrationProps } from '../mock_data';
jest.mock('~/integrations/edit/event_hub');
jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/url_utility');
describe('IntegrationForm', () => {
const mockToastShow = jest.fn();
@@ -34,12 +37,18 @@ describe('IntegrationForm', () => {
let dispatch;
let mockAxios;
let mockForm;
+ let vueIntegrationFormFeatureFlag;
+
+ const createForm = () => {
+ mockForm = document.createElement('form');
+ jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
+ };
const createComponent = ({
customStateProps = {},
- featureFlags = {},
initialState = {},
props = {},
+ mountFn = shallowMountExtended,
} = {}) => {
const store = createStore({
customState: { ...mockIntegrationProps, ...customStateProps },
@@ -47,11 +56,12 @@ describe('IntegrationForm', () => {
});
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMountExtended(IntegrationForm, {
- propsData: { ...props, formSelector: '.test' },
- provide: {
- glFeatures: featureFlags,
- },
+ if (!vueIntegrationFormFeatureFlag) {
+ createForm();
+ }
+
+ wrapper = mountFn(IntegrationForm, {
+ propsData: { ...props },
store,
stubs: {
OverrideDropdown,
@@ -65,26 +75,33 @@ describe('IntegrationForm', () => {
show: mockToastShow,
},
},
+ provide: {
+ glFeatures: {
+ vueIntegrationForm: vueIntegrationFormFeatureFlag,
+ },
+ },
});
};
- const createForm = ({ isValid = true } = {}) => {
- mockForm = document.createElement('form');
- jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
- jest.spyOn(mockForm, 'checkValidity').mockReturnValue(isValid);
- jest.spyOn(mockForm, 'submit');
- };
-
const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
const findResetButton = () => wrapper.findByTestId('reset-button');
- const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findProjectSaveButton = () => wrapper.findByTestId('save-button');
+ const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
const findTestButton = () => wrapper.findByTestId('test-button');
const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
+ const findGlForm = () => wrapper.findComponent(GlForm);
+ const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
+ const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
+
+ const mockFormFunctions = ({ checkValidityReturn }) => {
+ jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
+ jest.spyOn(findFormElement(), 'submit');
+ };
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -220,6 +237,7 @@ describe('IntegrationForm', () => {
createComponent({
customStateProps: { type: 'jira', testPath: '/test' },
+ mountFn: mountExtended,
});
});
@@ -338,6 +356,19 @@ describe('IntegrationForm', () => {
});
});
});
+
+ describe('when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', () => {
+ it('renders hidden fields', () => {
+ vueIntegrationFormFeatureFlag = true;
+ createComponent({
+ customStateProps: {
+ redirectTo: '/services',
+ },
+ });
+
+ expect(findRedirectToField().attributes('value')).toBe('/services');
+ });
+ });
});
describe('ActiveCheckbox', () => {
@@ -358,190 +389,292 @@ describe('IntegrationForm', () => {
});
describe.each`
- formActive | novalidate
- ${true} | ${null}
- ${false} | ${'true'}
+ formActive | vueIntegrationFormEnabled | novalidate
+ ${true} | ${true} | ${null}
+ ${false} | ${true} | ${'novalidate'}
+ ${true} | ${false} | ${null}
+ ${false} | ${false} | ${'true'}
`(
- 'when `toggle-integration-active` is emitted with $formActive',
- ({ formActive, novalidate }) => {
+ 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled and `toggle-integration-active` is emitted with $formActive',
+ ({ formActive, vueIntegrationFormEnabled, novalidate }) => {
beforeEach(async () => {
- createForm();
+ vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
+
createComponent({
customStateProps: {
showActive: true,
initialActivated: false,
},
+ mountFn: mountExtended,
});
+ mockFormFunctions({ checkValidityReturn: false });
await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive);
});
it(`sets noValidate to ${novalidate}`, () => {
- expect(mockForm.getAttribute('novalidate')).toBe(novalidate);
+ expect(findFormElement().getAttribute('novalidate')).toBe(novalidate);
});
},
);
});
- describe('when `save` button is clicked', () => {
- describe('buttons', () => {
- beforeEach(async () => {
- createForm();
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
+ describe.each`
+ vueIntegrationFormEnabled
+ ${true}
+ ${false}
+ `(
+ 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled',
+ ({ vueIntegrationFormEnabled }) => {
+ beforeEach(() => {
+ vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
+ });
+
+ describe('when `save` button is clicked', () => {
+ describe('buttons', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ },
+ mountFn: mountExtended,
+ });
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
+
+ it('sets save button `loading` prop to `true`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(true);
+ });
+
+ it('sets test button `disabled` prop to `true`', () => {
+ expect(findTestButton().props('disabled')).toBe(true);
+ });
});
- await findSaveButton().vm.$emit('click', new Event('click'));
- });
+ describe.each`
+ checkValidityReturn | integrationActive
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
+ ({ integrationActive, checkValidityReturn }) => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: integrationActive,
+ },
+ mountFn: mountExtended,
+ });
+
+ mockFormFunctions({ checkValidityReturn });
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
+
+ it('submits form', () => {
+ expect(findFormElement().submit).toHaveBeenCalledTimes(1);
+ });
+ },
+ );
+
+ describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ },
+ mountFn: mountExtended,
+ });
+ mockFormFunctions({ checkValidityReturn: false });
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
- it('sets save button `loading` prop to `true`', () => {
- expect(findSaveButton().props('loading')).toBe(true);
- });
+ it('does not submit form', () => {
+ expect(findFormElement().submit).not.toHaveBeenCalled();
+ });
- it('sets test button `disabled` prop to `true`', () => {
- expect(findTestButton().props('disabled')).toBe(true);
- });
- });
+ it('sets save button `loading` prop to `false`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(false);
+ });
- describe.each`
- checkValidityReturn | integrationActive
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${false}
- `(
- 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
- ({ integrationActive, checkValidityReturn }) => {
- beforeEach(async () => {
- createForm({ isValid: checkValidityReturn });
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: integrationActive,
- },
+ it('sets test button `disabled` prop to `false`', () => {
+ expect(findTestButton().props('disabled')).toBe(false);
});
- await findSaveButton().vm.$emit('click', new Event('click'));
+ it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ });
});
+ });
- it('submit form', () => {
- expect(mockForm.submit).toHaveBeenCalledTimes(1);
- });
- },
- );
+ describe('when `test` button is clicked', () => {
+ describe('when form is invalid', () => {
+ it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ },
+ mountFn: mountExtended,
+ });
+ mockFormFunctions({ checkValidityReturn: false });
- describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
- beforeEach(async () => {
- createForm({ isValid: false });
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
+ findTestButton().vm.$emit('click', new Event('click'));
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ });
});
- await findSaveButton().vm.$emit('click', new Event('click'));
- });
+ describe('when form is valid', () => {
+ const mockTestPath = '/test';
- it('does not submit form', () => {
- expect(mockForm.submit).not.toHaveBeenCalled();
- });
+ beforeEach(() => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ testPath: mockTestPath,
+ },
+ mountFn: mountExtended,
+ });
+ mockFormFunctions({ checkValidityReturn: true });
+ });
- it('sets save button `loading` prop to `false`', () => {
- expect(findSaveButton().props('loading')).toBe(false);
- });
+ describe('buttons', () => {
+ beforeEach(async () => {
+ await findTestButton().vm.$emit('click', new Event('click'));
+ });
- it('sets test button `disabled` prop to `false`', () => {
- expect(findTestButton().props('disabled')).toBe(false);
- });
+ it('sets test button `loading` prop to `true`', () => {
+ expect(findTestButton().props('loading')).toBe(true);
+ });
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ it('sets save button `disabled` prop to `true`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe.each`
+ scenario | replyStatus | errorMessage | expectToast | expectSentry
+ ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
+ ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
+ ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
+ `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
+ beforeEach(async () => {
+ mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
+ error: Boolean(errorMessage),
+ message: errorMessage,
+ });
+
+ await findTestButton().vm.$emit('click', new Event('click'));
+ await waitForPromises();
+ });
+
+ it(`calls toast with '${expectToast}'`, () => {
+ expect(mockToastShow).toHaveBeenCalledWith(expectToast);
+ });
+
+ it('sets `loading` prop of test button to `false`', () => {
+ expect(findTestButton().props('loading')).toBe(false);
+ });
+
+ it('sets save button `disabled` prop to `false`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(false);
+ });
+
+ it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
+ expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
+ });
+ });
+ });
});
- });
- });
+ },
+ );
+
+ describe('when `reset-confirmation-modal` emits `reset` event', () => {
+ const mockResetPath = '/reset';
- describe('when `test` button is clicked', () => {
- describe('when form is invalid', () => {
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
- createForm({ isValid: false });
+ describe('buttons', () => {
+ beforeEach(async () => {
createComponent({
customStateProps: {
- showActive: true,
+ integrationLevel: integrationLevels.GROUP,
canTest: true,
+ resetPath: mockResetPath,
},
});
- findTestButton().vm.$emit('click', new Event('click'));
+ await findResetConfirmationModal().vm.$emit('reset');
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ it('sets reset button `loading` prop to `true`', () => {
+ expect(findResetButton().props('loading')).toBe(true);
});
- });
- describe('when form is valid', () => {
- const mockTestPath = '/test';
+ it('sets other button `disabled` props to `true`', () => {
+ expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(true);
+ expect(findTestButton().props('disabled')).toBe(true);
+ });
+ });
- beforeEach(() => {
- createForm({ isValid: true });
+ describe('when "reset settings" request fails', () => {
+ beforeEach(async () => {
+ mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
createComponent({
customStateProps: {
- showActive: true,
+ integrationLevel: integrationLevels.GROUP,
canTest: true,
- testPath: mockTestPath,
+ resetPath: mockResetPath,
},
});
- });
-
- describe('buttons', () => {
- beforeEach(async () => {
- await findTestButton().vm.$emit('click', new Event('click'));
- });
- it('sets test button `loading` prop to `true`', () => {
- expect(findTestButton().props('loading')).toBe(true);
- });
+ await findResetConfirmationModal().vm.$emit('reset');
+ await waitForPromises();
+ });
- it('sets save button `disabled` prop to `true`', () => {
- expect(findSaveButton().props('disabled')).toBe(true);
- });
+ it('displays a toast', () => {
+ expect(mockToastShow).toHaveBeenCalledWith(I18N_DEFAULT_ERROR_MESSAGE);
});
- describe.each`
- scenario | replyStatus | errorMessage | expectToast | expectSentry
- ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
- ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
- ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
- `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
- beforeEach(async () => {
- mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
- error: Boolean(errorMessage),
- message: errorMessage,
- });
+ it('captures exception in Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalledTimes(1);
+ });
- await findTestButton().vm.$emit('click', new Event('click'));
- await waitForPromises();
- });
+ it('sets reset button `loading` prop to `false`', () => {
+ expect(findResetButton().props('loading')).toBe(false);
+ });
- it(`calls toast with '${expectToast}'`, () => {
- expect(mockToastShow).toHaveBeenCalledWith(expectToast);
- });
+ it('sets button `disabled` props to `false`', () => {
+ expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(false);
+ expect(findTestButton().props('disabled')).toBe(false);
+ });
+ });
- it('sets `loading` prop of test button to `false`', () => {
- expect(findTestButton().props('loading')).toBe(false);
+ describe('when "reset settings" succeeds', () => {
+ beforeEach(async () => {
+ mockAxios.onPost(mockResetPath).replyOnce(httpStatus.OK);
+ createComponent({
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ resetPath: mockResetPath,
+ },
});
- it('sets save button `disabled` prop to `false`', () => {
- expect(findSaveButton().props('disabled')).toBe(false);
- });
+ await findResetConfirmationModal().vm.$emit('reset');
+ await waitForPromises();
+ });
- it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
- expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
- });
+ it('calls `refreshCurrentPage`', () => {
+ expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
index b413de2b286..a5627d8b669 100644
--- a/spec/frontend/integrations/edit/store/actions_spec.js
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -4,17 +4,12 @@ import testAction from 'helpers/vuex_action_helper';
import { I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } from '~/integrations/constants';
import {
setOverride,
- setIsResetting,
- requestResetIntegration,
- receiveResetIntegrationSuccess,
- receiveResetIntegrationError,
requestJiraIssueTypes,
receiveJiraIssueTypesSuccess,
receiveJiraIssueTypesError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
import createState from '~/integrations/edit/store/state';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { mockJiraIssueTypes } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -38,38 +33,6 @@ describe('Integration form store actions', () => {
});
});
- describe('setIsResetting', () => {
- it('should commit isResetting mutation', () => {
- return testAction(setIsResetting, true, state, [
- { type: types.SET_IS_RESETTING, payload: true },
- ]);
- });
- });
-
- describe('requestResetIntegration', () => {
- it('should commit REQUEST_RESET_INTEGRATION mutation', () => {
- return testAction(requestResetIntegration, null, state, [
- { type: types.REQUEST_RESET_INTEGRATION },
- ]);
- });
- });
-
- describe('receiveResetIntegrationSuccess', () => {
- it('should call refreshCurrentPage()', () => {
- return testAction(receiveResetIntegrationSuccess, null, state, [], [], () => {
- expect(refreshCurrentPage).toHaveBeenCalled();
- });
- });
- });
-
- describe('receiveResetIntegrationError', () => {
- it('should commit RECEIVE_RESET_INTEGRATION_ERROR mutation', () => {
- return testAction(receiveResetIntegrationError, null, state, [
- { type: types.RECEIVE_RESET_INTEGRATION_ERROR },
- ]);
- });
- });
-
describe('requestJiraIssueTypes', () => {
describe.each`
scenario | responseCode | response | action
diff --git a/spec/frontend/integrations/edit/store/mutations_spec.js b/spec/frontend/integrations/edit/store/mutations_spec.js
index 641547550d1..ecac9d88982 100644
--- a/spec/frontend/integrations/edit/store/mutations_spec.js
+++ b/spec/frontend/integrations/edit/store/mutations_spec.js
@@ -17,30 +17,6 @@ describe('Integration form store mutations', () => {
});
});
- describe(`${types.SET_IS_RESETTING}`, () => {
- it('sets isResetting', () => {
- mutations[types.SET_IS_RESETTING](state, true);
-
- expect(state.isResetting).toBe(true);
- });
- });
-
- describe(`${types.REQUEST_RESET_INTEGRATION}`, () => {
- it('sets isResetting', () => {
- mutations[types.REQUEST_RESET_INTEGRATION](state);
-
- expect(state.isResetting).toBe(true);
- });
- });
-
- describe(`${types.RECEIVE_RESET_INTEGRATION_ERROR}`, () => {
- it('sets isResetting', () => {
- mutations[types.RECEIVE_RESET_INTEGRATION_ERROR](state);
-
- expect(state.isResetting).toBe(false);
- });
- });
-
describe(`${types.SET_JIRA_ISSUE_TYPES}`, () => {
it('sets jiraIssueTypes', () => {
const jiraIssueTypes = ['issue', 'epic'];
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index 5582be7fd3c..0b4ca8fb65c 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -5,8 +5,6 @@ describe('Integration form state factory', () => {
expect(createState()).toEqual({
defaultState: null,
customState: {},
- isSaving: false,
- isResetting: false,
override: false,
isLoadingJiraIssueTypes: false,
jiraIssueTypes: [],
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index 8abd83887f7..6aa3e661677 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -5,6 +5,8 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_PER_PAGE } from '~/api';
import IntegrationOverrides from '~/integrations/overrides/components/integration_overrides.vue';
+import IntegrationTabs from '~/integrations/overrides/components/integration_tabs.vue';
+
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
@@ -49,6 +51,7 @@ describe('IntegrationOverrides', () => {
const findGlTable = () => wrapper.findComponent(GlTable);
const findPagination = () => wrapper.findComponent(GlPagination);
+ const findIntegrationTabs = () => wrapper.findComponent(IntegrationTabs);
const findRowsAsModel = () =>
findGlTable()
.findAllComponents(GlLink)
@@ -72,6 +75,12 @@ describe('IntegrationOverrides', () => {
expect(table.exists()).toBe(true);
expect(table.attributes('busy')).toBe('true');
});
+
+ it('renders IntegrationTabs with count as `null`', () => {
+ createComponent();
+
+ expect(findIntegrationTabs().props('projectOverridesCount')).toBe(null);
+ });
});
describe('when initial request is successful', () => {
@@ -84,6 +93,13 @@ describe('IntegrationOverrides', () => {
expect(table.attributes('busy')).toBeFalsy();
});
+ it('renders IntegrationTabs with count', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findIntegrationTabs().props('projectOverridesCount')).toBe(mockOverrides.length);
+ });
+
describe('table template', () => {
beforeEach(async () => {
createComponent({ mountFn: mount });
diff --git a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
new file mode 100644
index 00000000000..a728b4d391f
--- /dev/null
+++ b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
@@ -0,0 +1,64 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlBadge, GlTab } from '@gitlab/ui';
+
+import IntegrationTabs from '~/integrations/overrides/components/integration_tabs.vue';
+import { settingsTabTitle, overridesTabTitle } from '~/integrations/constants';
+
+describe('IntegrationTabs', () => {
+ let wrapper;
+
+ const editPath = 'mock/edit';
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(IntegrationTabs, {
+ propsData: props,
+ provide: {
+ editPath,
+ },
+ stubs: {
+ GlTab,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findGlTab = () => wrapper.findComponent(GlTab);
+ const findSettingsLink = () => wrapper.find('a');
+
+ describe('template', () => {
+ it('renders "Settings" tab as a link', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findSettingsLink().text()).toMatchInterpolatedText(settingsTabTitle);
+ expect(findSettingsLink().attributes('href')).toBe(editPath);
+ });
+
+ it('renders "Projects using custom settings" tab as active', () => {
+ const projectOverridesCount = '1';
+
+ createComponent({
+ props: { projectOverridesCount },
+ });
+
+ expect(findGlTab().exists()).toBe(true);
+ expect(findGlTab().text()).toMatchInterpolatedText(
+ `${overridesTabTitle} ${projectOverridesCount}`,
+ );
+ expect(findGlBadge().text()).toBe(projectOverridesCount);
+ });
+
+ describe('when count is `null', () => {
+ it('renders "Projects using custom settings" tab without count', () => {
+ createComponent();
+
+ expect(findGlTab().exists()).toBe(true);
+ expect(findGlTab().text()).toMatchInterpolatedText(overridesTabTitle);
+ expect(findGlBadge().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index e190ddf243e..3ab89b3dff2 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -474,6 +474,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user1] });
});
@@ -644,6 +646,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user3] });
});
@@ -712,6 +716,8 @@ describe('InviteMembersModal', () => {
it('displays the invalid syntax error if one of the emails is invalid', async () => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user3, user4] });
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.ERROR_EMAIL_INVALID);
@@ -787,6 +793,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user1, user3] });
mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
@@ -815,6 +823,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createComponent({ groupToBeSharedWith: sharedGroup });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
@@ -837,6 +847,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteGroupToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ groupToBeSharedWith: sharedGroup });
wrapper.vm.$toast = { show: jest.fn() };
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index dd84b4fd78f..a3e426376d8 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -26,7 +26,7 @@ const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
const INVITATIONS_API_EMAIL_TAKEN = {
message: {
- 'email@example2.com': 'Invite email has already been taken',
+ 'email@example.org': 'Invite email has already been taken',
},
status: 'error',
};
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 6ac4c9e8546..6a896ccd21a 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -169,6 +169,8 @@ describe('RelatedIssuableItem', () => {
});
it('renders disabled button when removeDisabled', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ removeDisabled: true });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index 9f07eea433a..fdc0bd7d72e 100644
--- a/spec/frontend/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import confidentialState from '~/confidential_merge_request/state';
-import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
+import CreateMergeRequestDropdown from '~/issues/create_merge_request_dropdown';
import axios from '~/lib/utils/axios_utils';
describe('CreateMergeRequestDropdown', () => {
diff --git a/spec/frontend/issues_list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index 7c5faeb8dc1..e9c48b60da4 100644
--- a/spec/frontend/issues_list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -1,7 +1,7 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
-import IssueCardTimeInfo from '~/issues_list/components/issue_card_time_info.vue';
+import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
describe('CE IssueCardTimeInfo component', () => {
useFakeDate(2020, 11, 11);
diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index f24c090fa92..66428ee0492 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -5,8 +5,8 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -17,15 +17,15 @@ import {
filteredTokens,
locationSearch,
urlParams,
-} from 'jest/issues_list/mock_data';
+} from 'jest/issues/list/mock_data';
import createFlash, { FLASH_TYPES } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
-import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
-import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue';
+import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
DUE_DATE_OVERDUE,
@@ -41,9 +41,9 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
urlSortParams,
-} from '~/issues_list/constants';
-import eventHub from '~/issues_list/eventhub';
-import { getSortOptions } from '~/issues_list/utils';
+} from '~/issues/list/constants';
+import eventHub from '~/issues/list/eventhub';
+import { getSortOptions } from '~/issues/list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index 633799816d8..d6d6bb14e9d 100644
--- a/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import JiraIssuesImportStatus from '~/issues_list/components/jira_issues_import_status_app.vue';
+import JiraIssuesImportStatus from '~/issues/list/components/jira_issues_import_status_app.vue';
describe('JiraIssuesImportStatus', () => {
const issuesPath = 'gitlab-org/gitlab-test/-/issues';
diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
index 1c9a87e8af2..0c52e66ff14 100644
--- a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
+++ b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
@@ -2,8 +2,8 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue';
-import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import {
emptySearchProjectsQueryResponse,
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 948699876ce..948699876ce 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index 8e1d70db92d..0e4979fd7b4 100644
--- a/spec/frontend/issues_list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -7,14 +7,14 @@ import {
locationSearchWithSpecialValues,
urlParams,
urlParamsWithSpecialValues,
-} from 'jest/issues_list/mock_data';
+} from 'jest/issues/list/mock_data';
import {
defaultPageSizeParams,
DUE_DATE_VALUES,
largePageSizeParams,
RELATIVE_POSITION_ASC,
urlSortParams,
-} from '~/issues_list/constants';
+} from '~/issues/list/constants';
import {
convertToApiParams,
convertToSearchQuery,
@@ -24,7 +24,7 @@ import {
getInitialPageParams,
getSortKey,
getSortOptions,
-} from '~/issues_list/utils';
+} from '~/issues/list/utils';
describe('getInitialPageParams', () => {
it.each(Object.keys(urlSortParams))(
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index 984d0c9d25b..f6b93cc5a62 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -38,6 +38,8 @@ describe('Issue title suggestions component', () => {
});
it('renders component', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -47,6 +49,8 @@ describe('Issue title suggestions component', () => {
it('does not render with empty search', () => {
wrapper.setProps({ search: '' });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -55,6 +59,8 @@ describe('Issue title suggestions component', () => {
});
it('does not render when loading', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
...data,
loading: 1,
@@ -66,6 +72,8 @@ describe('Issue title suggestions component', () => {
});
it('does not render with empty issues data', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ issues: [] });
return wrapper.vm.$nextTick(() => {
@@ -74,6 +82,8 @@ describe('Issue title suggestions component', () => {
});
it('renders list of issues', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -82,6 +92,8 @@ describe('Issue title suggestions component', () => {
});
it('adds margin class to first item', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -90,6 +102,8 @@ describe('Issue title suggestions component', () => {
});
it('does not add margin class to last item', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 3ece10e70db..7f7b16583e6 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssueTypeField, { i18n } from '~/issues/show/components/fields/type.vue';
-import { IssuableTypes } from '~/issues/show/constants';
+import { issuableTypes } from '~/issues/show/constants';
import {
getIssueStateQueryResponse,
updateIssueStateQueryResponse,
@@ -69,8 +69,8 @@ describe('Issue type field component', () => {
it.each`
at | text | icon
- ${0} | ${IssuableTypes[0].text} | ${IssuableTypes[0].icon}
- ${1} | ${IssuableTypes[1].text} | ${IssuableTypes[1].icon}
+ ${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
+ ${1} | ${issuableTypes[1].text} | ${issuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
@@ -81,20 +81,20 @@ describe('Issue type field component', () => {
});
it('renders a form select with the `issue_type` value', () => {
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
describe('with Apollo cache mock', () => {
it('renders the selected issueType', async () => {
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
- findTypeFromDropDownItems().at(1).vm.$emit('click', IssuableTypes.incident);
+ findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
await wrapper.vm.$nextTick();
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
describe('when user is a guest', () => {
@@ -104,7 +104,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
@@ -113,7 +113,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 2a16c699c4d..d09bf6faa13 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -4,11 +4,10 @@ import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import createFlash, { FLASH_TYPES } from '~/flash';
-import { IssuableType } from '~/vue_shared/issuable/show/constants';
+import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
-import { IssuableStatus } from '~/issues/constants';
-import { IssueStateEvent } from '~/issues/show/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
@@ -36,7 +35,7 @@ describe('HeaderActions component', () => {
iid: '32',
isIssueAuthor: true,
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
- issueType: IssuableType.Issue,
+ issueType: IssueType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath:
@@ -112,14 +111,14 @@ describe('HeaderActions component', () => {
describe.each`
issueType
- ${IssuableType.Issue}
- ${IssuableType.Incident}
+ ${IssueType.Issue}
+ ${IssueType.Incident}
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
description | issueState | buttonText | newIssueState
- ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${IssueStateEvent.Close}
- ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${IssueStateEvent.Reopen}
+ ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
+ ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
@@ -306,7 +305,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
- stateEvent: IssueStateEvent.Close,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
@@ -345,7 +344,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid.toString(),
projectPath: defaultProps.projectPath,
- stateEvent: IssueStateEvent.Close,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
diff --git a/spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
index 5a51ae3cfe0..b38d2b60057 100644
--- a/spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
@@ -1,11 +1,9 @@
+import Vue from 'vue';
import { GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
-import SentryErrorStackTrace from '~/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import SentryErrorStackTrace from '~/issues/show/components/sentry_error_stack_trace.vue';
describe('Sentry Error Stack Trace', () => {
let actions;
@@ -13,13 +11,14 @@ describe('Sentry Error Stack Trace', () => {
let store;
let wrapper;
+ Vue.use(Vuex);
+
function mountComponent({
stubs = {
stacktrace: Stacktrace,
},
} = {}) {
wrapper = shallowMount(SentryErrorStackTrace, {
- localVue,
stubs,
store,
propsData: {
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 6d7a31a6c8c..68c2e3768c7 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { initIssuableApp } from '~/issues/show/issue';
+import { initIssueApp } from '~/issues/show';
import * as parseData from '~/issues/show/utils/parse_data';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
@@ -17,7 +17,7 @@ const setupHTML = (initialData) => {
};
describe('Issue show index', () => {
- describe('initIssuableApp', () => {
+ describe('initIssueApp', () => {
it('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
@@ -29,7 +29,7 @@ describe('Issue show index', () => {
const initialDataEl = document.getElementById('js-issuable-app');
const issuableData = parseData.parseIssuableData(initialDataEl);
- initIssuableApp(issuableData, createStore());
+ initIssueApp(issuableData, createStore());
await waitForPromises();
diff --git a/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap b/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap
deleted file mode 100644
index c327b7de827..00000000000
--- a/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Issuables list component with empty issues response with all state should display a catch-all if there are no issues to show 1`] = `
-<gl-empty-state-stub
- svgpath="/emptySvg"
- title="There are no issues to show"
-/>
-`;
-
-exports[`Issuables list component with empty issues response with closed state should display a message "There are no closed issues" if there are no closed issues 1`] = `"There are no closed issues"`;
-
-exports[`Issuables list component with empty issues response with empty query should display the message "There are no open issues" 1`] = `"There are no open issues"`;
-
-exports[`Issuables list component with empty issues response with query in window location should display "Sorry, your filter produced no results" if filters are too specific 1`] = `"Sorry, your filter produced no results"`;
diff --git a/spec/frontend/issues_list/components/issuable_spec.js b/spec/frontend/issues_list/components/issuable_spec.js
deleted file mode 100644
index f3c2ae1f9dc..00000000000
--- a/spec/frontend/issues_list/components/issuable_spec.js
+++ /dev/null
@@ -1,508 +0,0 @@
-import { GlSprintf, GlLabel, GlIcon, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import { trimText } from 'helpers/text_helper';
-import Issuable from '~/issues_list/components/issuable.vue';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { formatDate } from '~/lib/utils/datetime_utility';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import initUserPopovers from '~/user_popovers';
-import IssueAssignees from '~/issuable/components/issue_assignees.vue';
-import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
-
-jest.mock('~/user_popovers');
-
-const TODAY = new Date();
-
-const createTestDateFromDelta = (timeDelta) =>
- formatDate(new Date(TODAY.getTime() + timeDelta), 'yyyy-mm-dd');
-
-// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
-const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31;
-const TEST_MONTH_AGO = createTestDateFromDelta(-MONTHS_IN_MS);
-const TEST_MONTH_LATER = createTestDateFromDelta(MONTHS_IN_MS);
-const DATE_FORMAT = 'mmm d, yyyy';
-const TEST_USER_NAME = 'Tyler Durden';
-const TEST_BASE_URL = `${TEST_HOST}/issues`;
-const TEST_TASK_STATUS = '50 of 100 tasks completed';
-const TEST_MILESTONE = {
- title: 'Milestone title',
- web_url: `${TEST_HOST}/milestone/1`,
-};
-const TEXT_CLOSED = 'CLOSED';
-const TEST_META_COUNT = 100;
-const MOCK_GITLAB_URL = 'http://0.0.0.0:3000';
-
-describe('Issuable component', () => {
- let issuable;
- let wrapper;
-
- const factory = (props = {}, scopedLabelsAvailable = false) => {
- wrapper = shallowMount(Issuable, {
- propsData: {
- issuable: simpleIssue,
- baseUrl: TEST_BASE_URL,
- ...props,
- },
- provide: {
- scopedLabelsAvailable,
- },
- stubs: {
- 'gl-sprintf': GlSprintf,
- },
- });
- };
-
- beforeEach(() => {
- issuable = { ...simpleIssue };
- gon.gitlab_url = MOCK_GITLAB_URL;
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const checkExists = (findFn) => () => findFn().exists();
- const hasIcon = (iconName, iconWrapper = wrapper) =>
- iconWrapper.findAll(GlIcon).wrappers.some((icon) => icon.props('name') === iconName);
- const hasConfidentialIcon = () => hasIcon('eye-slash');
- const findTaskStatus = () => wrapper.find('.task-status');
- const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]');
- const findAuthor = () => wrapper.find({ ref: 'openedAgoByContainer' });
- const findMilestone = () => wrapper.find('.js-milestone');
- const findMilestoneTooltip = () => findMilestone().attributes('title');
- const findDueDate = () => wrapper.find('.js-due-date');
- const findLabels = () => wrapper.findAll(GlLabel);
- const findWeight = () => wrapper.find('[data-testid="weight"]');
- const findAssignees = () => wrapper.find(IssueAssignees);
- const findBlockingIssuesCount = () => wrapper.find('[data-testid="blocking-issues"]');
- const findMergeRequestsCount = () => wrapper.find('[data-testid="merge-requests"]');
- const findUpvotes = () => wrapper.find('[data-testid="upvotes"]');
- const findDownvotes = () => wrapper.find('[data-testid="downvotes"]');
- const findNotes = () => wrapper.find('[data-testid="notes-count"]');
- const findBulkCheckbox = () => wrapper.find('input.selected-issuable');
- const findScopedLabels = () => findLabels().filter((w) => isScopedLabel({ title: w.text() }));
- const findUnscopedLabels = () => findLabels().filter((w) => !isScopedLabel({ title: w.text() }));
- const findIssuableTitle = () => wrapper.find('[data-testid="issuable-title"]');
- const findIssuableStatus = () => wrapper.find('[data-testid="issuable-status"]');
- const containsJiraLogo = () => wrapper.find('[data-testid="jira-logo"]').exists();
- const findHealthStatus = () => wrapper.find('.health-status');
-
- describe('when mounted', () => {
- it('initializes user popovers', () => {
- expect(initUserPopovers).not.toHaveBeenCalled();
-
- factory();
-
- expect(initUserPopovers).toHaveBeenCalledWith([wrapper.vm.$refs.openedAgoByContainer.$el]);
- });
- });
-
- describe('when scopedLabels feature is available', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
-
- factory({ issuable }, true);
- });
-
- describe('when label is scoped', () => {
- it('returns label with correct props', () => {
- const scopedLabel = findScopedLabels().at(0);
-
- expect(scopedLabel.props('scoped')).toBe(true);
- });
- });
-
- describe('when label is not scoped', () => {
- it('returns label with correct props', () => {
- const notScopedLabel = findUnscopedLabels().at(0);
-
- expect(notScopedLabel.props('scoped')).toBe(false);
- });
- });
- });
-
- describe('when scopedLabels feature is not available', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
-
- factory({ issuable });
- });
-
- describe('when label is scoped', () => {
- it('label scoped props is false', () => {
- const scopedLabel = findScopedLabels().at(0);
-
- expect(scopedLabel.props('scoped')).toBe(false);
- });
- });
-
- describe('when label is not scoped', () => {
- it('label scoped props is false', () => {
- const notScopedLabel = findUnscopedLabels().at(0);
-
- expect(notScopedLabel.props('scoped')).toBe(false);
- });
- });
- });
-
- describe('with simple issuable', () => {
- beforeEach(() => {
- Object.assign(issuable, {
- has_tasks: false,
- task_status: TEST_TASK_STATUS,
- created_at: TEST_MONTH_AGO,
- author: {
- ...issuable.author,
- name: TEST_USER_NAME,
- },
- labels: [],
- });
-
- factory({ issuable });
- });
-
- it.each`
- desc | check
- ${'bulk editing checkbox'} | ${checkExists(findBulkCheckbox)}
- ${'confidential icon'} | ${hasConfidentialIcon}
- ${'task status'} | ${checkExists(findTaskStatus)}
- ${'milestone'} | ${checkExists(findMilestone)}
- ${'due date'} | ${checkExists(findDueDate)}
- ${'labels'} | ${checkExists(findLabels)}
- ${'weight'} | ${checkExists(findWeight)}
- ${'blocking issues count'} | ${checkExists(findBlockingIssuesCount)}
- ${'merge request count'} | ${checkExists(findMergeRequestsCount)}
- ${'upvotes'} | ${checkExists(findUpvotes)}
- ${'downvotes'} | ${checkExists(findDownvotes)}
- `('does not render $desc', ({ check }) => {
- expect(check()).toBe(false);
- });
-
- it('show relative reference path', () => {
- expect(wrapper.find('.js-ref-path').text()).toBe(issuable.references.relative);
- });
-
- it('does not have closed text', () => {
- expect(wrapper.text()).not.toContain(TEXT_CLOSED);
- });
-
- it('does not have closed class', () => {
- expect(wrapper.classes('closed')).toBe(false);
- });
-
- it('renders fuzzy created date and author', () => {
- expect(trimText(findOpenedAgoContainer().text())).toContain(
- `created 1 month ago by ${TEST_USER_NAME}`,
- );
- });
-
- it('renders no comments', () => {
- expect(findNotes().classes('no-comments')).toBe(true);
- });
-
- it.each`
- gitlabWebUrl | webUrl | expectedHref | expectedTarget | isExternal
- ${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined} | ${false}
- ${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'} | ${true}
- ${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined} | ${false}
- `(
- 'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`',
- async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget, isExternal }) => {
- factory({
- issuable: {
- ...issuable,
- web_url: webUrl,
- gitlab_web_url: gitlabWebUrl,
- },
- });
-
- const titleEl = findIssuableTitle();
-
- expect(titleEl.exists()).toBe(true);
- expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref);
- expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget);
- expect(titleEl.find(GlLink).text()).toBe(issuable.title);
-
- expect(titleEl.find(GlIcon).exists()).toBe(isExternal);
- },
- );
- });
-
- describe('with confidential issuable', () => {
- beforeEach(() => {
- issuable.confidential = true;
-
- factory({ issuable });
- });
-
- it('renders the confidential icon', () => {
- expect(hasConfidentialIcon()).toBe(true);
- });
- });
-
- describe('with Jira issuable', () => {
- beforeEach(() => {
- issuable.external_tracker = 'jira';
-
- factory({ issuable });
- });
-
- it('renders the Jira icon', () => {
- expect(containsJiraLogo()).toBe(true);
- });
-
- it('opens issuable in a new tab', () => {
- expect(findIssuableTitle().props('target')).toBe('_blank');
- });
-
- it('opens author in a new tab', () => {
- expect(findAuthor().props('target')).toBe('_blank');
- });
-
- describe('with Jira status', () => {
- const expectedStatus = 'In Progress';
-
- beforeEach(() => {
- issuable.status = expectedStatus;
-
- factory({ issuable });
- });
-
- it('renders the Jira status', () => {
- expect(findIssuableStatus().text()).toBe(expectedStatus);
- });
- });
- });
-
- describe('with task status', () => {
- beforeEach(() => {
- Object.assign(issuable, {
- has_tasks: true,
- task_status: TEST_TASK_STATUS,
- });
-
- factory({ issuable });
- });
-
- it('renders task status', () => {
- expect(findTaskStatus().exists()).toBe(true);
- expect(findTaskStatus().text()).toBe(TEST_TASK_STATUS);
- });
- });
-
- describe.each`
- desc | dueDate | expectedTooltipPart
- ${'past due'} | ${TEST_MONTH_AGO} | ${'Past due'}
- ${'future due'} | ${TEST_MONTH_LATER} | ${'1 month remaining'}
- `('with milestone with $desc', ({ dueDate, expectedTooltipPart }) => {
- beforeEach(() => {
- issuable.milestone = { ...TEST_MILESTONE, due_date: dueDate };
-
- factory({ issuable });
- });
-
- it('renders milestone', () => {
- expect(findMilestone().exists()).toBe(true);
- expect(hasIcon('clock', findMilestone())).toBe(true);
- expect(findMilestone().text()).toEqual(TEST_MILESTONE.title);
- });
-
- it('renders tooltip', () => {
- expect(findMilestoneTooltip()).toBe(
- `${formatDate(dueDate, DATE_FORMAT)} (${expectedTooltipPart})`,
- );
- });
-
- it('renders milestone with the correct href', () => {
- const { title } = issuable.milestone;
- const expected = mergeUrlParams({ milestone_title: title }, TEST_BASE_URL);
-
- expect(findMilestone().attributes('href')).toBe(expected);
- });
- });
-
- describe.each`
- dueDate | hasClass | desc
- ${TEST_MONTH_LATER} | ${false} | ${'with future due date'}
- ${TEST_MONTH_AGO} | ${true} | ${'with past due date'}
- `('$desc', ({ dueDate, hasClass }) => {
- beforeEach(() => {
- issuable.due_date = dueDate;
-
- factory({ issuable });
- });
-
- it('renders due date', () => {
- expect(findDueDate().exists()).toBe(true);
- expect(findDueDate().text()).toBe(formatDate(dueDate, DATE_FORMAT));
- });
-
- it(hasClass ? 'has cred class' : 'does not have cred class', () => {
- expect(findDueDate().classes('cred')).toEqual(hasClass);
- });
- });
-
- describe('with labels', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
-
- factory({ issuable });
- });
-
- it('renders labels', () => {
- factory({ issuable });
-
- const labels = findLabels().wrappers.map((label) => ({
- href: label.props('target'),
- text: label.text(),
- tooltip: label.attributes('description'),
- }));
-
- const expected = testLabels.map((label) => ({
- href: mergeUrlParams({ 'label_name[]': label.name }, TEST_BASE_URL),
- text: label.name,
- tooltip: label.description,
- }));
-
- expect(labels).toEqual(expected);
- });
- });
-
- describe('with labels for Jira issuable', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
- issuable.external_tracker = 'jira';
-
- factory({ issuable });
- });
-
- it('renders labels', () => {
- factory({ issuable });
-
- const labels = findLabels().wrappers.map((label) => ({
- href: label.props('target'),
- text: label.text(),
- tooltip: label.attributes('description'),
- }));
-
- const expected = testLabels.map((label) => ({
- href: mergeUrlParams({ 'labels[]': label.name }, TEST_BASE_URL),
- text: label.name,
- tooltip: label.description,
- }));
-
- expect(labels).toEqual(expected);
- });
- });
-
- describe.each`
- weight
- ${0}
- ${10}
- ${12345}
- `('with weight $weight', ({ weight }) => {
- beforeEach(() => {
- issuable.weight = weight;
-
- factory({ issuable });
- });
-
- it('renders weight', () => {
- expect(findWeight().exists()).toBe(true);
- expect(findWeight().text()).toEqual(weight.toString());
- });
- });
-
- describe('with closed state', () => {
- beforeEach(() => {
- issuable.state = 'closed';
-
- factory({ issuable });
- });
-
- it('renders closed text', () => {
- expect(wrapper.text()).toContain(TEXT_CLOSED);
- });
-
- it('has closed class', () => {
- expect(wrapper.classes('closed')).toBe(true);
- });
- });
-
- describe('with assignees', () => {
- beforeEach(() => {
- issuable.assignees = testAssignees;
-
- factory({ issuable });
- });
-
- it('renders assignees', () => {
- expect(findAssignees().exists()).toBe(true);
- expect(findAssignees().props('assignees')).toEqual(testAssignees);
- });
- });
-
- describe.each`
- desc | key | finder
- ${'with blocking issues count'} | ${'blocking_issues_count'} | ${findBlockingIssuesCount}
- ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount}
- ${'with upvote count'} | ${'upvotes'} | ${findUpvotes}
- ${'with downvote count'} | ${'downvotes'} | ${findDownvotes}
- ${'with notes count'} | ${'user_notes_count'} | ${findNotes}
- `('$desc', ({ key, finder }) => {
- beforeEach(() => {
- issuable[key] = TEST_META_COUNT;
-
- factory({ issuable });
- });
-
- it('renders correct count', () => {
- expect(finder().exists()).toBe(true);
- expect(finder().text()).toBe(TEST_META_COUNT.toString());
- expect(finder().classes('no-comments')).toBe(false);
- });
- });
-
- describe('with bulk editing', () => {
- describe.each`
- selected | desc
- ${true} | ${'when selected'}
- ${false} | ${'when unselected'}
- `('$desc', ({ selected }) => {
- beforeEach(() => {
- factory({ isBulkEditing: true, selected });
- });
-
- it(`renders checked is ${selected}`, () => {
- expect(findBulkCheckbox().element.checked).toBe(selected);
- });
-
- it('emits select when clicked', () => {
- expect(wrapper.emitted().select).toBeUndefined();
-
- findBulkCheckbox().trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().select).toEqual([[{ issuable, selected: !selected }]]);
- });
- });
- });
- });
-
- if (IS_EE) {
- describe('with health status', () => {
- it('renders health status tag', () => {
- factory({ issuable });
- expect(findHealthStatus().exists()).toBe(true);
- });
-
- it('does not render when health status is absent', () => {
- issuable.health_status = null;
- factory({ issuable });
- expect(findHealthStatus().exists()).toBe(false);
- });
- });
- }
-});
diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
deleted file mode 100644
index 11854db534e..00000000000
--- a/spec/frontend/issues_list/components/issuables_list_app_spec.js
+++ /dev/null
@@ -1,653 +0,0 @@
-import {
- GlEmptyState,
- GlPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
-} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
-import Issuable from '~/issues_list/components/issuable.vue';
-import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
-import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
-import issuablesEventBus from '~/issues_list/eventhub';
-import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-
-jest.mock('~/flash');
-jest.mock('~/issues_list/eventhub');
-jest.mock('~/lib/utils/common_utils', () => ({
- ...jest.requireActual('~/lib/utils/common_utils'),
- scrollToElement: () => {},
-}));
-
-const TEST_LOCATION = `${TEST_HOST}/issues`;
-const TEST_ENDPOINT = '/issues';
-const TEST_CREATE_ISSUES_PATH = '/createIssue';
-const TEST_SVG_PATH = '/emptySvg';
-
-const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
- .fill(0)
- .map((_, i) => ({
- id: i,
- web_url: `url${i}`,
- }));
-
-describe('Issuables list component', () => {
- let mockAxios;
- let wrapper;
- let apiSpy;
-
- const setupApiMock = (cb) => {
- apiSpy = jest.fn(cb);
-
- mockAxios.onGet(TEST_ENDPOINT).reply((cfg) => apiSpy(cfg));
- };
-
- const factory = (props = { sortKey: 'priority' }) => {
- const emptyStateMeta = {
- createIssuePath: TEST_CREATE_ISSUES_PATH,
- svgPath: TEST_SVG_PATH,
- };
-
- wrapper = shallowMount(IssuablesListApp, {
- propsData: {
- endpoint: TEST_ENDPOINT,
- emptyStateMeta,
- ...props,
- },
- });
- };
-
- const findLoading = () => wrapper.find(GlSkeletonLoading);
- const findIssuables = () => wrapper.findAll(Issuable);
- const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
- const findFirstIssuable = () => findIssuables().wrappers[0];
- const findEmptyState = () => wrapper.find(GlEmptyState);
-
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
-
- setWindowLocation(TEST_LOCATION);
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- mockAxios.restore();
- });
-
- describe('with failed issues response', () => {
- beforeEach(() => {
- setupApiMock(() => [500]);
-
- factory();
-
- return waitForPromises();
- });
-
- it('does not show loading', () => {
- expect(wrapper.vm.loading).toBe(false);
- });
-
- it('flashes an error', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('with successful issues response', () => {
- beforeEach(() => {
- setupApiMock(() => [
- 200,
- MOCK_ISSUES.slice(0, PAGE_SIZE),
- {
- 'x-total': 100,
- 'x-page': 2,
- },
- ]);
- });
-
- it('has default props and data', () => {
- factory();
- expect(wrapper.vm).toMatchObject({
- // Props
- canBulkEdit: false,
- emptyStateMeta: {
- createIssuePath: TEST_CREATE_ISSUES_PATH,
- svgPath: TEST_SVG_PATH,
- },
- // Data
- filters: {
- state: 'opened',
- },
- isBulkEditing: false,
- issuables: [],
- loading: true,
- page: 1,
- selection: {},
- totalItems: 0,
- });
- });
-
- it('does not call API until mounted', () => {
- factory();
- expect(apiSpy).not.toHaveBeenCalled();
- });
-
- describe('when mounted', () => {
- beforeEach(() => {
- factory();
- });
-
- it('calls API', () => {
- expect(apiSpy).toHaveBeenCalled();
- });
-
- it('shows loading', () => {
- expect(findLoading().exists()).toBe(true);
- expect(findIssuables().length).toBe(0);
- expect(findEmptyState().exists()).toBe(false);
- });
- });
-
- describe('when finished loading', () => {
- beforeEach(() => {
- factory();
-
- return waitForPromises();
- });
-
- it('does not display empty state', () => {
- expect(wrapper.vm.issuables.length).toBeGreaterThan(0);
- expect(wrapper.vm.emptyState).toEqual({});
- expect(wrapper.find(GlEmptyState).exists()).toBe(false);
- });
-
- it('sets the proper page and total items', () => {
- expect(wrapper.vm.totalItems).toBe(100);
- expect(wrapper.vm.page).toBe(2);
- });
-
- it('renders one page of issuables and pagination', () => {
- expect(findIssuables().length).toBe(PAGE_SIZE);
- expect(wrapper.find(GlPagination).exists()).toBe(true);
- });
- });
-
- it('does not render FilteredSearchBar', () => {
- factory();
-
- expect(findFilteredSearchBar().exists()).toBe(false);
- });
- });
-
- describe('with bulk editing enabled', () => {
- beforeEach(() => {
- issuablesEventBus.$on.mockReset();
- issuablesEventBus.$emit.mockReset();
-
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ canBulkEdit: true });
-
- return waitForPromises();
- });
-
- it('is not enabled by default', () => {
- expect(wrapper.vm.isBulkEditing).toBe(false);
- });
-
- it('does not select issues by default', () => {
- expect(wrapper.vm.selection).toEqual({});
- });
-
- it('"Select All" checkbox toggles all visible issuables"', () => {
- wrapper.vm.onSelectAll();
- expect(wrapper.vm.selection).toEqual(
- wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}),
- );
-
- wrapper.vm.onSelectAll();
- expect(wrapper.vm.selection).toEqual({});
- });
-
- it('"Select All checkbox" selects all issuables if only some are selected"', () => {
- wrapper.vm.selection = { [wrapper.vm.issuables[0].id]: true };
- wrapper.vm.onSelectAll();
- expect(wrapper.vm.selection).toEqual(
- wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}),
- );
- });
-
- it('selects and deselects issuables', () => {
- const [i0, i1, i2] = wrapper.vm.issuables;
-
- expect(wrapper.vm.selection).toEqual({});
- wrapper.vm.onSelectIssuable({ issuable: i0, selected: false });
- expect(wrapper.vm.selection).toEqual({});
- wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true });
- wrapper.vm.onSelectIssuable({ issuable: i0, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true, 0: true });
- wrapper.vm.onSelectIssuable({ issuable: i2, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true, 0: true, 2: true });
- wrapper.vm.onSelectIssuable({ issuable: i2, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true, 0: true, 2: true });
- wrapper.vm.onSelectIssuable({ issuable: i0, selected: false });
- expect(wrapper.vm.selection).toEqual({ 1: true, 2: true });
- });
-
- it('broadcasts a message to the bulk edit sidebar when a value is added to selection', () => {
- issuablesEventBus.$emit.mockReset();
- const i1 = wrapper.vm.issuables[1];
-
- wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(issuablesEventBus.$emit).toHaveBeenCalledTimes(1);
- expect(issuablesEventBus.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
- });
- });
-
- it('does not broadcast a message to the bulk edit sidebar when a value is not added to selection', () => {
- issuablesEventBus.$emit.mockReset();
-
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- const i1 = wrapper.vm.issuables[1];
-
- wrapper.vm.onSelectIssuable({ issuable: i1, selected: false });
- })
- .then(wrapper.vm.$nextTick)
- .then(() => {
- expect(issuablesEventBus.$emit).toHaveBeenCalledTimes(0);
- });
- });
-
- it('listens to a message to toggle bulk editing', () => {
- expect(wrapper.vm.isBulkEditing).toBe(false);
- expect(issuablesEventBus.$on.mock.calls[0][0]).toBe('issuables:toggleBulkEdit');
- issuablesEventBus.$on.mock.calls[0][1](true); // Call the message handler
-
- return waitForPromises()
- .then(() => {
- expect(wrapper.vm.isBulkEditing).toBe(true);
- issuablesEventBus.$on.mock.calls[0][1](false);
- })
- .then(() => {
- expect(wrapper.vm.isBulkEditing).toBe(false);
- });
- });
- });
-
- describe('with query params in window.location', () => {
- const expectedFilters = {
- assignee_username: 'root',
- author_username: 'root',
- confidential: 'yes',
- my_reaction_emoji: 'airplane',
- scope: 'all',
- state: 'opened',
- weight: '0',
- milestone: 'v3.0',
- labels: 'Aquapod,Astro',
- order_by: 'milestone_due',
- sort: 'desc',
- };
-
- describe('when page is not present in params', () => {
- const query =
- '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
-
- beforeEach(() => {
- setWindowLocation(query);
-
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ sortKey: 'milestone_due_desc' });
-
- return waitForPromises();
- });
-
- afterEach(() => {
- apiSpy.mockClear();
- });
-
- it('applies filters and sorts', () => {
- expect(wrapper.vm.hasFilters).toBe(true);
- expect(wrapper.vm.filters).toEqual({
- ...expectedFilters,
- 'not[milestone]': ['13'],
- 'not[labels]': ['Afterpod'],
- });
-
- expect(apiSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- params: {
- ...expectedFilters,
- with_labels_details: true,
- page: 1,
- per_page: PAGE_SIZE,
- 'not[milestone]': ['13'],
- 'not[labels]': ['Afterpod'],
- },
- }),
- );
- });
-
- it('passes the base url to issuable', () => {
- expect(findFirstIssuable().props('baseUrl')).toBe(TEST_LOCATION);
- });
- });
-
- describe('when page is present in the param', () => {
- const query =
- '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&page=3';
-
- beforeEach(() => {
- setWindowLocation(query);
-
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ sortKey: 'milestone_due_desc' });
-
- return waitForPromises();
- });
-
- afterEach(() => {
- apiSpy.mockClear();
- });
-
- it('applies filters and sorts', () => {
- expect(apiSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- params: {
- ...expectedFilters,
- with_labels_details: true,
- page: 3,
- per_page: PAGE_SIZE,
- },
- }),
- );
- });
- });
- });
-
- describe('with hash in window.location', () => {
- beforeEach(() => {
- setWindowLocation(`${TEST_LOCATION}#stuff`);
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory();
- return waitForPromises();
- });
-
- it('passes the base url to issuable', () => {
- expect(findFirstIssuable().props('baseUrl')).toBe(TEST_LOCATION);
- });
- });
-
- describe('with manual sort', () => {
- beforeEach(() => {
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ sortKey: RELATIVE_POSITION });
- });
-
- it('uses manual page size', () => {
- expect(apiSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- params: expect.objectContaining({
- per_page: PAGE_SIZE_MANUAL,
- }),
- }),
- );
- });
- });
-
- describe('with empty issues response', () => {
- beforeEach(() => {
- setupApiMock(() => [200, []]);
- });
-
- describe('with query in window location', () => {
- beforeEach(() => {
- setWindowLocation('?weight=Any');
-
- factory();
-
- return waitForPromises().then(() => wrapper.vm.$nextTick());
- });
-
- it('should display "Sorry, your filter produced no results" if filters are too specific', () => {
- expect(findEmptyState().props('title')).toMatchSnapshot();
- });
- });
-
- describe('with closed state', () => {
- beforeEach(() => {
- setWindowLocation('?state=closed');
-
- factory();
-
- return waitForPromises().then(() => wrapper.vm.$nextTick());
- });
-
- it('should display a message "There are no closed issues" if there are no closed issues', () => {
- expect(findEmptyState().props('title')).toMatchSnapshot();
- });
- });
-
- describe('with all state', () => {
- beforeEach(() => {
- setWindowLocation('?state=all');
-
- factory();
-
- return waitForPromises().then(() => wrapper.vm.$nextTick());
- });
-
- it('should display a catch-all if there are no issues to show', () => {
- expect(findEmptyState().element).toMatchSnapshot();
- });
- });
-
- describe('with empty query', () => {
- beforeEach(() => {
- factory();
-
- return wrapper.vm.$nextTick().then(waitForPromises);
- });
-
- it('should display the message "There are no open issues"', () => {
- expect(findEmptyState().props('title')).toMatchSnapshot();
- });
- });
- });
-
- describe('when paginates', () => {
- const newPage = 3;
-
- describe('when total-items is defined in response headers', () => {
- beforeEach(() => {
- window.history.pushState = jest.fn();
- setupApiMock(() => [
- 200,
- MOCK_ISSUES.slice(0, PAGE_SIZE),
- {
- 'x-total': 100,
- 'x-page': 2,
- },
- ]);
-
- factory();
-
- return waitForPromises();
- });
-
- afterEach(() => {
- // reset to original value
- window.history.pushState.mockRestore();
- });
-
- it('calls window.history.pushState one time', () => {
- // Trigger pagination
- wrapper.find(GlPagination).vm.$emit('input', newPage);
-
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- });
-
- it('sets params in the url', () => {
- // Trigger pagination
- wrapper.find(GlPagination).vm.$emit('input', newPage);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened&order_by=priority&sort=asc&page=${newPage}`,
- );
- });
- });
-
- describe('when total-items is not defined in the headers', () => {
- const page = 2;
- const prevPage = page - 1;
- const nextPage = page + 1;
-
- beforeEach(() => {
- setupApiMock(() => [
- 200,
- MOCK_ISSUES.slice(0, PAGE_SIZE),
- {
- 'x-page': page,
- },
- ]);
-
- factory();
-
- return waitForPromises();
- });
-
- it('finds the correct props applied to GlPagination', () => {
- expect(wrapper.find(GlPagination).props()).toMatchObject({
- nextPage,
- prevPage,
- value: page,
- });
- });
- });
- });
-
- describe('when type is "jira"', () => {
- it('renders FilteredSearchBar', () => {
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().exists()).toBe(true);
- });
-
- describe('initialSortBy', () => {
- const query = '?sort=updated_asc';
-
- it('sets default value', () => {
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialSortBy')).toBe('created_desc');
- });
-
- it('sets value according to query', () => {
- setWindowLocation(query);
-
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialSortBy')).toBe('updated_asc');
- });
- });
-
- describe('initialFilterValue', () => {
- it('does not set value when no query', () => {
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([]);
- });
-
- it('sets value according to query', () => {
- const query = '?search=free+text';
-
- setWindowLocation(query);
-
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']);
- });
- });
-
- describe('on filter search', () => {
- beforeEach(() => {
- factory({ type: 'jira' });
-
- window.history.pushState = jest.fn();
- });
-
- afterEach(() => {
- window.history.pushState.mockRestore();
- });
-
- const emitOnFilter = (filter) => findFilteredSearchBar().vm.$emit('onFilter', filter);
-
- describe('empty filter', () => {
- const mockFilter = [];
-
- it('updates URL with correct params', () => {
- emitOnFilter(mockFilter);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened`,
- );
- });
- });
-
- describe('filter with search term', () => {
- const mockFilter = [
- {
- type: 'filtered-search-term',
- value: { data: 'free' },
- },
- ];
-
- it('updates URL with correct params', () => {
- emitOnFilter(mockFilter);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened&search=free`,
- );
- });
- });
-
- describe('filter with multiple search terms', () => {
- const mockFilter = [
- {
- type: 'filtered-search-term',
- value: { data: 'free' },
- },
- {
- type: 'filtered-search-term',
- value: { data: 'text' },
- },
- ];
-
- it('updates URL with correct params', () => {
- emitOnFilter(mockFilter);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened&search=free+text`,
- );
- });
- });
- });
- });
-});
diff --git a/spec/frontend/issues_list/issuable_list_test_data.js b/spec/frontend/issues_list/issuable_list_test_data.js
deleted file mode 100644
index 313aa15bd31..00000000000
--- a/spec/frontend/issues_list/issuable_list_test_data.js
+++ /dev/null
@@ -1,77 +0,0 @@
-export const simpleIssue = {
- id: 442,
- iid: 31,
- title: 'Dismiss Cipher with no integrity',
- state: 'opened',
- created_at: '2019-08-26T19:06:32.667Z',
- updated_at: '2019-08-28T19:53:58.314Z',
- labels: [],
- milestone: null,
- assignees: [],
- author: {
- id: 3,
- name: 'Elnora Bernhard',
- username: 'treva.lesch',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/a8c0d9c2882406cf2a9b71494625a796?s=80&d=identicon',
- web_url: 'http://localhost:3001/treva.lesch',
- },
- assignee: null,
- user_notes_count: 0,
- blocking_issues_count: 0,
- merge_requests_count: 0,
- upvotes: 0,
- downvotes: 0,
- due_date: null,
- confidential: false,
- web_url: 'http://localhost:3001/h5bp/html5-boilerplate/issues/31',
- has_tasks: false,
- weight: null,
- references: {
- relative: 'html-boilerplate#45',
- },
- health_status: 'on_track',
-};
-
-export const testLabels = [
- {
- id: 1,
- name: 'Tanuki',
- description: 'A cute animal',
- color: '#ff0000',
- text_color: '#ffffff',
- },
- {
- id: 2,
- name: 'Octocat',
- description: 'A grotesque mish-mash of whiskers and tentacles',
- color: '#333333',
- text_color: '#000000',
- },
- {
- id: 3,
- name: 'scoped::label',
- description: 'A scoped label',
- color: '#00ff00',
- text_color: '#ffffff',
- },
-];
-
-export const testAssignees = [
- {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3001/root',
- },
- {
- id: 22,
- name: 'User 0',
- username: 'user0',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon',
- web_url: 'http://localhost:3001/user0',
- },
-];
diff --git a/spec/frontend/issues_list/service_desk_helper_spec.js b/spec/frontend/issues_list/service_desk_helper_spec.js
deleted file mode 100644
index 16aee853341..00000000000
--- a/spec/frontend/issues_list/service_desk_helper_spec.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { emptyStateHelper, generateMessages } from '~/issues_list/service_desk_helper';
-
-describe('service desk helper', () => {
- const emptyStateMessages = generateMessages({});
-
- // Note: isServiceDeskEnabled must not be true when isServiceDeskSupported is false (it's an invalid case).
- describe.each`
- isServiceDeskSupported | isServiceDeskEnabled | canEditProjectSettings | expectedMessage
- ${true} | ${true} | ${true} | ${'serviceDeskEnabledAndCanEditProjectSettings'}
- ${true} | ${true} | ${false} | ${'serviceDeskEnabledAndCannotEditProjectSettings'}
- ${true} | ${false} | ${true} | ${'serviceDeskDisabledAndCanEditProjectSettings'}
- ${true} | ${false} | ${false} | ${'serviceDeskDisabledAndCannotEditProjectSettings'}
- ${false} | ${false} | ${true} | ${'serviceDeskIsNotSupported'}
- ${false} | ${false} | ${false} | ${'serviceDeskIsNotEnabled'}
- `(
- 'isServiceDeskSupported = $isServiceDeskSupported, isServiceDeskEnabled = $isServiceDeskEnabled, canEditProjectSettings = $canEditProjectSettings',
- ({ isServiceDeskSupported, isServiceDeskEnabled, canEditProjectSettings, expectedMessage }) => {
- it(`displays ${expectedMessage} message`, () => {
- const emptyStateMeta = {
- isServiceDeskEnabled,
- isServiceDeskSupported,
- canEditProjectSettings,
- };
- expect(emptyStateHelper(emptyStateMeta)).toEqual(emptyStateMessages[expectedMessage]);
- });
- },
- );
-});
diff --git a/spec/frontend/jira_import/utils/jira_import_utils_spec.js b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
index 9696d95f8c4..4207038f50c 100644
--- a/spec/frontend/jira_import/utils/jira_import_utils_spec.js
+++ b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
@@ -1,5 +1,5 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/jira_import/utils/constants';
import {
calculateJiraImportLabel,
extractJiraProjectsOptions,
diff --git a/spec/frontend/jobs/bridge/app_spec.js b/spec/frontend/jobs/bridge/app_spec.js
index 0e232ab240d..c0faab90552 100644
--- a/spec/frontend/jobs/bridge/app_spec.js
+++ b/spec/frontend/jobs/bridge/app_spec.js
@@ -1,27 +1,104 @@
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import getPipelineQuery from '~/jobs/bridge/graphql/queries/pipeline.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
import BridgeApp from '~/jobs/bridge/app.vue';
import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue';
import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import {
+ MOCK_BUILD_ID,
+ MOCK_PIPELINE_IID,
+ MOCK_PROJECT_FULL_PATH,
+ mockPipelineQueryResponse,
+} from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Bridge Show Page', () => {
let wrapper;
+ let mockApollo;
+ let mockPipelineQuery;
+
+ const createComponent = (options) => {
+ wrapper = shallowMount(BridgeApp, {
+ provide: {
+ buildId: MOCK_BUILD_ID,
+ projectFullPath: MOCK_PROJECT_FULL_PATH,
+ pipelineIid: MOCK_PIPELINE_IID,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ pipeline: {
+ loading: true,
+ },
+ },
+ },
+ },
+ ...options,
+ });
+ };
- const createComponent = () => {
- wrapper = shallowMount(BridgeApp, {});
+ const createComponentWithApollo = () => {
+ const handlers = [[getPipelineQuery, mockPipelineQuery]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ localVue,
+ apolloProvider: mockApollo,
+ mocks: {},
+ });
};
+ const findCiHeader = () => wrapper.findComponent(CiHeader);
const findEmptyState = () => wrapper.findComponent(BridgeEmptyState);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSidebar = () => wrapper.findComponent(BridgeSidebar);
+ beforeEach(() => {
+ mockPipelineQuery = jest.fn();
+ });
+
afterEach(() => {
+ mockPipelineQuery.mockReset();
wrapper.destroy();
});
- describe('template', () => {
+ describe('while pipeline query is loading', () => {
beforeEach(() => {
createComponent();
});
+ it('renders loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('after pipeline query is loaded', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
+ createComponentWithApollo();
+ waitForPromises();
+ });
+
+ it('query is called with correct variables', async () => {
+ expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
+ expect(mockPipelineQuery).toHaveBeenCalledWith({
+ fullPath: MOCK_PROJECT_FULL_PATH,
+ iid: MOCK_PIPELINE_IID,
+ });
+ });
+
+ it('renders CI header state', () => {
+ expect(findCiHeader().exists()).toBe(true);
+ });
+
it('renders empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
@@ -30,4 +107,42 @@ describe('Bridge Show Page', () => {
expect(findSidebar().exists()).toBe(true);
});
});
+
+ describe('sidebar expansion', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
+ createComponentWithApollo();
+ waitForPromises();
+ });
+
+ describe('on resize', () => {
+ it.each`
+ breakpoint | isSidebarExpanded
+ ${'xs'} | ${false}
+ ${'sm'} | ${false}
+ ${'md'} | ${true}
+ ${'lg'} | ${true}
+ ${'xl'} | ${true}
+ `(
+ 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"',
+ async ({ breakpoint, isSidebarExpanded }) => {
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
+
+ window.dispatchEvent(new Event('resize'));
+ await nextTick();
+
+ expect(findSidebar().exists()).toBe(isSidebarExpanded);
+ },
+ );
+ });
+
+ it('toggles expansion on button click', async () => {
+ expect(findSidebar().exists()).toBe(true);
+
+ wrapper.vm.toggleSidebar();
+ await nextTick();
+
+ expect(findSidebar().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/jobs/bridge/components/empty_state_spec.js b/spec/frontend/jobs/bridge/components/empty_state_spec.js
index 83642450118..38c55b296f0 100644
--- a/spec/frontend/jobs/bridge/components/empty_state_spec.js
+++ b/spec/frontend/jobs/bridge/components/empty_state_spec.js
@@ -6,14 +6,13 @@ import { MOCK_EMPTY_ILLUSTRATION_PATH, MOCK_PATH_TO_DOWNSTREAM } from '../mock_d
describe('Bridge Empty State', () => {
let wrapper;
- const createComponent = (props) => {
+ const createComponent = ({ downstreamPipelinePath }) => {
wrapper = shallowMount(BridgeEmptyState, {
provide: {
emptyStateIllustrationPath: MOCK_EMPTY_ILLUSTRATION_PATH,
},
propsData: {
- downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM,
- ...props,
+ downstreamPipelinePath,
},
});
};
@@ -28,7 +27,7 @@ describe('Bridge Empty State', () => {
describe('template', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM });
});
it('renders illustration', () => {
diff --git a/spec/frontend/jobs/bridge/components/sidebar_spec.js b/spec/frontend/jobs/bridge/components/sidebar_spec.js
index ba4018753af..5006d4f08a6 100644
--- a/spec/frontend/jobs/bridge/components/sidebar_spec.js
+++ b/spec/frontend/jobs/bridge/components/sidebar_spec.js
@@ -1,24 +1,38 @@
import { GlButton, GlDropdown } from '@gitlab/ui';
-import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
-import { BUILD_NAME } from '../mock_data';
+import CommitBlock from '~/jobs/components/commit_block.vue';
+import { mockCommit, mockJob } from '../mock_data';
describe('Bridge Sidebar', () => {
let wrapper;
- const createComponent = () => {
+ const MockHeaderEl = {
+ getBoundingClientRect() {
+ return {
+ bottom: '40',
+ };
+ },
+ };
+
+ const createComponent = ({ featureFlag } = {}) => {
wrapper = shallowMount(BridgeSidebar, {
provide: {
- buildName: BUILD_NAME,
+ glFeatures: {
+ triggerJobRetryAction: featureFlag,
+ },
+ },
+ propsData: {
+ bridgeJob: mockJob,
+ commit: mockCommit,
},
});
};
- const findSidebar = () => wrapper.find('aside');
+ const findJobTitle = () => wrapper.find('h4');
+ const findCommitBlock = () => wrapper.findComponent(CommitBlock);
const findRetryDropdown = () => wrapper.find(GlDropdown);
- const findToggle = () => wrapper.find(GlButton);
+ const findToggleBtn = () => wrapper.findComponent(GlButton);
afterEach(() => {
wrapper.destroy();
@@ -29,8 +43,23 @@ describe('Bridge Sidebar', () => {
createComponent();
});
- it('renders retry dropdown', () => {
- expect(findRetryDropdown().exists()).toBe(true);
+ it('renders job name', () => {
+ expect(findJobTitle().text()).toBe(mockJob.name);
+ });
+
+ it('renders commit information', () => {
+ expect(findCommitBlock().exists()).toBe(true);
+ });
+ });
+
+ describe('styles', () => {
+ beforeEach(async () => {
+ jest.spyOn(document, 'querySelector').mockReturnValue(MockHeaderEl);
+ createComponent();
+ });
+
+ it('calculates root styles correctly', () => {
+ expect(wrapper.attributes('style')).toBe('width: 290px; top: 40px;');
});
});
@@ -39,38 +68,32 @@ describe('Bridge Sidebar', () => {
createComponent();
});
- it('toggles expansion on button click', async () => {
- expect(findSidebar().classes()).not.toContain('gl-display-none');
+ it('emits toggle sidebar event on button click', async () => {
+ expect(wrapper.emitted('toggleSidebar')).toBe(undefined);
- findToggle().vm.$emit('click');
- await nextTick();
+ findToggleBtn().vm.$emit('click');
- expect(findSidebar().classes()).toContain('gl-display-none');
+ expect(wrapper.emitted('toggleSidebar')).toHaveLength(1);
});
+ });
- describe('on resize', () => {
- it.each`
- breakpoint | isSidebarExpanded
- ${'xs'} | ${false}
- ${'sm'} | ${false}
- ${'md'} | ${true}
- ${'lg'} | ${true}
- ${'xl'} | ${true}
- `(
- 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"',
- async ({ breakpoint, isSidebarExpanded }) => {
- jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
-
- window.dispatchEvent(new Event('resize'));
- await nextTick();
-
- if (isSidebarExpanded) {
- expect(findSidebar().classes()).not.toContain('gl-display-none');
- } else {
- expect(findSidebar().classes()).toContain('gl-display-none');
- }
- },
- );
+ describe('retry action', () => {
+ describe('when feature flag is ON', () => {
+ beforeEach(() => {
+ createComponent({ featureFlag: true });
+ });
+
+ it('renders retry dropdown', () => {
+ expect(findRetryDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when feature flag is OFF', () => {
+ it('does not render retry dropdown', () => {
+ createComponent({ featureFlag: false });
+
+ expect(findRetryDropdown().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/jobs/bridge/mock_data.js b/spec/frontend/jobs/bridge/mock_data.js
index 146d1a062ac..4084bb54163 100644
--- a/spec/frontend/jobs/bridge/mock_data.js
+++ b/spec/frontend/jobs/bridge/mock_data.js
@@ -1,3 +1,102 @@
export const MOCK_EMPTY_ILLUSTRATION_PATH = '/path/to/svg';
export const MOCK_PATH_TO_DOWNSTREAM = '/path/to/downstream/pipeline';
-export const BUILD_NAME = 'Child Pipeline Trigger';
+export const MOCK_BUILD_ID = '1331';
+export const MOCK_PIPELINE_IID = '174';
+export const MOCK_PROJECT_FULL_PATH = '/root/project/';
+export const MOCK_SHA = '38f3d89147765427a7ce58be28cd76d14efa682a';
+
+export const mockCommit = {
+ id: `gid://gitlab/CommitPresenter/${MOCK_SHA}`,
+ shortId: '38f3d891',
+ title: 'Update .gitlab-ci.yml file',
+ webPath: `/root/project/-/commit/${MOCK_SHA}`,
+ __typename: 'Commit',
+};
+
+export const mockJob = {
+ createdAt: '2021-12-10T09:05:45Z',
+ id: 'gid://gitlab/Ci::Build/1331',
+ name: 'triggerJobName',
+ scheduledAt: null,
+ startedAt: '2021-12-10T09:13:43Z',
+ status: 'SUCCESS',
+ triggered: null,
+ detailedStatus: {
+ id: '1',
+ detailsPath: '/root/project/-/jobs/1331',
+ icon: 'status_success',
+ group: 'success',
+ text: 'passed',
+ tooltip: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ downstreamPipeline: {
+ id: '1',
+ path: '/root/project/-/pipelines/175',
+ },
+ stage: {
+ id: '1',
+ name: 'build',
+ __typename: 'CiStage',
+ },
+ __typename: 'CiJob',
+};
+
+export const mockUser = {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webPath: '/root',
+ webUrl: 'http://gdk.test:3000/root',
+ status: {
+ message: 'making great things',
+ __typename: 'UserStatus',
+ },
+ __typename: 'UserCore',
+};
+
+export const mockStage = {
+ id: '1',
+ name: 'build',
+ jobs: {
+ nodes: [mockJob],
+ __typename: 'CiJobConnection',
+ },
+ __typename: 'CiStage',
+};
+
+export const mockPipelineQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ commit: mockCommit,
+ id: 'gid://gitlab/Ci::Pipeline/174',
+ iid: '88',
+ path: '/root/project/-/pipelines/174',
+ sha: MOCK_SHA,
+ ref: 'main',
+ refPath: 'path/to/ref',
+ user: mockUser,
+ detailedStatus: {
+ id: '1',
+ icon: 'status_failed',
+ group: 'failed',
+ __typename: 'DetailedStatus',
+ },
+ stages: {
+ edges: [
+ {
+ node: mockStage,
+ __typename: 'CiStageEdge',
+ },
+ ],
+ __typename: 'CiStageConnection',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 482d0df4e9a..05988eecb10 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -114,6 +114,8 @@ describe('Job table app', () => {
await wrapper.vm.$nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
jobs: {
pageInfo: {
diff --git a/spec/frontend/labels/delete_label_modal_spec.js b/spec/frontend/labels/delete_label_modal_spec.js
index c1e6ce87990..98049538948 100644
--- a/spec/frontend/labels/delete_label_modal_spec.js
+++ b/spec/frontend/labels/delete_label_modal_spec.js
@@ -13,6 +13,10 @@ describe('DeleteLabelModal', () => {
subjectName: 'GitLab Org',
destroyPath: `${TEST_HOST}/2`,
},
+ {
+ labelName: 'admin label',
+ destroyPath: `${TEST_HOST}/3`,
+ },
];
beforeEach(() => {
@@ -22,8 +26,12 @@ describe('DeleteLabelModal', () => {
const button = document.createElement('button');
button.setAttribute('class', 'js-delete-label-modal-button');
button.setAttribute('data-label-name', x.labelName);
- button.setAttribute('data-subject-name', x.subjectName);
button.setAttribute('data-destroy-path', x.destroyPath);
+
+ if (x.subjectName) {
+ button.setAttribute('data-subject-name', x.subjectName);
+ }
+
button.innerHTML = 'Action';
buttonContainer.appendChild(button);
});
@@ -62,6 +70,7 @@ describe('DeleteLabelModal', () => {
index
${0}
${1}
+ ${2}
`(`when multiple buttons exist`, ({ index }) => {
beforeEach(() => {
initDeleteLabelModal();
@@ -69,14 +78,22 @@ describe('DeleteLabelModal', () => {
});
it('correct props are passed to gl-modal', () => {
- expect(findModal().querySelector('.modal-title').innerHTML).toContain(
- buttons[index].labelName,
- );
- expect(findModal().querySelector('.modal-body').innerHTML).toContain(
- buttons[index].subjectName,
- );
+ const button = buttons[index];
+
+ expect(findModal().querySelector('.modal-title').innerHTML).toContain(button.labelName);
+
+ if (button.subjectName) {
+ expect(findModal().querySelector('.modal-body').textContent).toContain(
+ `${button.labelName} will be permanently deleted from ${button.subjectName}. This cannot be undone.`,
+ );
+ } else {
+ expect(findModal().querySelector('.modal-body').textContent).toContain(
+ `${button.labelName} will be permanently deleted. This cannot be undone.`,
+ );
+ }
+
expect(findModal().querySelector('.modal-footer .btn-danger').href).toContain(
- buttons[index].destroyPath,
+ button.destroyPath,
);
});
});
diff --git a/spec/frontend/lib/utils/resize_observer_spec.js b/spec/frontend/lib/utils/resize_observer_spec.js
new file mode 100644
index 00000000000..419aff28935
--- /dev/null
+++ b/spec/frontend/lib/utils/resize_observer_spec.js
@@ -0,0 +1,68 @@
+import { contentTop } from '~/lib/utils/common_utils';
+import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
+
+jest.mock('~/lib/utils/common_utils');
+
+function mockStickyHeaderSize(val) {
+ contentTop.mockReturnValue(val);
+}
+
+describe('ResizeObserver Utility', () => {
+ let observer;
+ const triggerResize = () => {
+ const entry = document.querySelector('#content-body');
+ entry.dispatchEvent(new CustomEvent(`ResizeUpdate`, { detail: { entry } }));
+ };
+
+ beforeEach(() => {
+ mockStickyHeaderSize(90);
+
+ jest.spyOn(document.documentElement, 'scrollTo');
+
+ setFixtures(`<div id="content-body"><div class="target">element to scroll to</div></div>`);
+
+ const target = document.querySelector('.target');
+
+ jest.spyOn(target, 'getBoundingClientRect').mockReturnValue({ top: 200 });
+
+ observer = scrollToTargetOnResize({
+ target: '.target',
+ container: '#content-body',
+ });
+ });
+
+ afterEach(() => {
+ contentTop.mockReset();
+ });
+
+ describe('Observer behavior', () => {
+ it('returns null for empty target', () => {
+ observer = scrollToTargetOnResize({
+ target: '',
+ container: '#content-body',
+ });
+
+ expect(observer).toBe(null);
+ });
+
+ it('returns ResizeObserver instance', () => {
+ expect(observer).toBeInstanceOf(ResizeObserver);
+ });
+
+ it('scrolls body so anchor is just below sticky header (contentTop)', () => {
+ triggerResize();
+
+ expect(document.documentElement.scrollTo).toHaveBeenCalledWith({ top: 110 });
+ });
+
+ const interactionEvents = ['mousedown', 'touchstart', 'keydown', 'wheel'];
+ it.each(interactionEvents)('does not hijack scroll after user input from %s', (eventType) => {
+ const event = new Event(eventType);
+ document.dispatchEvent(event);
+
+ triggerResize();
+
+ expect(document.documentElement.scrollTo).not.toHaveBeenCalledWith();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index aaa0a91ffe0..681fb05a6c4 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -128,7 +128,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg"
emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg"
selectedstate="gettingStarted"
- settingspath="/monitoring/monitor-project/-/services/prometheus/edit"
+ settingspath="/monitoring/monitor-project/-/integrations/prometheus/edit"
/>
</div>
`;
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 27f7489aa49..ff6f0b9b0c7 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -661,6 +661,8 @@ describe('Time series component', () => {
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
tooltip: {
type: 'deployments',
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 9331048bce3..7730e7f347f 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -792,6 +792,8 @@ describe('Dashboard', () => {
});
createShallowWrapper({ hasMetrics: true });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hoveredPanel: panelRef });
return wrapper.vm.$nextTick();
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 589354e7849..f6d30384847 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -38,6 +38,8 @@ describe('DashboardsDropdown', () => {
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
const setSearchTerm = (searchTerm) => wrapper.setData({ searchTerm });
beforeEach(() => {
diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js
index 0c6e4211b10..36ad82e93a5 100644
--- a/spec/frontend/mr_popover/mr_popover_spec.js
+++ b/spec/frontend/mr_popover/mr_popover_spec.js
@@ -35,6 +35,8 @@ describe('MR Popover', () => {
describe('loaded state', () => {
it('matches the snapshot', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
mergeRequest: {
title: 'Updated Title',
@@ -55,6 +57,8 @@ describe('MR Popover', () => {
});
it('does not show CI Icon if there is no pipeline data', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
mergeRequest: {
state: 'opened',
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index c3a51c51de0..16dbf60cef4 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -263,6 +263,8 @@ describe('issue_comment_form component', () => {
jest.spyOn(wrapper.vm, 'stopPolling');
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ note: 'hello world' });
await findCommentButton().trigger('click');
@@ -388,6 +390,8 @@ describe('issue_comment_form component', () => {
it('should enable comment button if it has note', async () => {
mountComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ note: 'Foo' });
expect(findCommentTypeDropdown().props('disabled')).toBe(false);
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 48bfd6eac5a..d3b5ab02f24 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -257,6 +257,8 @@ describe('issue_note_form component', () => {
props = { ...props, ...options };
wrapper = createComponentWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isSubmittingWithKeydown: true });
const textarea = wrapper.find('textarea');
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 0782ec7cdd5..7a036d25559 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -88,6 +88,8 @@ describe('CustomNotificationsModal', () => {
beforeEach(async () => {
wrapper = createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
events: [
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
@@ -211,6 +213,8 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent({ injectedProperties });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
events: [
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
@@ -239,6 +243,8 @@ describe('CustomNotificationsModal', () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
events: [
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index f06300efa29..5278e730ec9 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,7 +1,6 @@
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlDropdown } from 'jest/packages_and_registries/container_registry/explorer/stubs';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -51,6 +50,7 @@ describe('Details Header', () => {
const findCleanup = () => findByTestId('cleanup');
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
+ const findMenu = () => wrapper.findComponent(GlDropdown);
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -139,51 +139,53 @@ describe('Details Header', () => {
});
});
- describe('delete button', () => {
- it('exists', () => {
- mountComponent();
+ describe('menu', () => {
+ it.each`
+ canDelete | disabled | isVisible
+ ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ `(
+ 'when canDelete is $canDelete and disabled is $disabled is $isVisible that the menu is visible',
+ ({ canDelete, disabled, isVisible }) => {
+ mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
- expect(findDeleteButton().exists()).toBe(true);
- });
+ expect(findMenu().exists()).toBe(isVisible);
+ },
+ );
- it('has the correct text', () => {
- mountComponent();
+ describe('delete button', () => {
+ it('exists', () => {
+ mountComponent();
- expect(findDeleteButton().text()).toBe('Delete image repository');
- });
+ expect(findDeleteButton().exists()).toBe(true);
+ });
- it('has the correct props', () => {
- mountComponent();
+ it('has the correct text', () => {
+ mountComponent();
- expect(findDeleteButton().attributes()).toMatchObject(
- expect.objectContaining({
- variant: 'danger',
- }),
- );
- });
+ expect(findDeleteButton().text()).toBe('Delete image repository');
+ });
- it('emits the correct event', () => {
- mountComponent();
+ it('has the correct props', () => {
+ mountComponent();
- findDeleteButton().vm.$emit('click');
+ expect(findDeleteButton().attributes()).toMatchObject(
+ expect.objectContaining({
+ variant: 'danger',
+ }),
+ );
+ });
- expect(wrapper.emitted('delete')).toEqual([[]]);
- });
+ it('emits the correct event', () => {
+ mountComponent();
- it.each`
- canDelete | disabled | isDisabled
- ${true} | ${false} | ${undefined}
- ${true} | ${true} | ${'true'}
- ${false} | ${false} | ${'true'}
- ${false} | ${true} | ${'true'}
- `(
- 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
- ({ canDelete, disabled, isDisabled }) => {
- mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
+ findDeleteButton().vm.$emit('click');
- expect(findDeleteButton().attributes('disabled')).toBe(isDisabled);
- },
- );
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
+ });
});
describe('metadata items', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
deleted file mode 100644
index f14284e9efe..00000000000
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import component from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
-import {
- NO_TAGS_TITLE,
- NO_TAGS_MESSAGE,
- MISSING_OR_DELETED_IMAGE_TITLE,
- MISSING_OR_DELETED_IMAGE_MESSAGE,
-} from '~/packages_and_registries/container_registry/explorer/constants';
-
-describe('EmptyTagsState component', () => {
- let wrapper;
-
- const findEmptyState = () => wrapper.find(GlEmptyState);
-
- const mountComponent = (propsData) => {
- wrapper = shallowMount(component, {
- stubs: {
- GlEmptyState,
- },
- propsData,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('contains gl-empty-state', () => {
- mountComponent();
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it.each`
- isEmptyImage | title | description
- ${false} | ${NO_TAGS_TITLE} | ${NO_TAGS_MESSAGE}
- ${true} | ${MISSING_OR_DELETED_IMAGE_TITLE} | ${MISSING_OR_DELETED_IMAGE_MESSAGE}
- `(
- 'when isEmptyImage is $isEmptyImage has the correct props',
- ({ isEmptyImage, title, description }) => {
- mountComponent({
- noContainersImage: 'foo',
- isEmptyImage,
- });
-
- expect(findEmptyState().props()).toMatchObject({
- title,
- description,
- svgPath: 'foo',
- });
- },
- );
-});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index 00b1d03b7c2..057312828ff 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -75,16 +75,19 @@ describe('tags list row', () => {
});
it.each`
- digest | disabled
- ${'foo'} | ${true}
- ${null} | ${false}
- ${null} | ${true}
- ${'foo'} | ${true}
- `('is disabled when the digest $digest and disabled is $disabled', ({ digest, disabled }) => {
- mountComponent({ tag: { ...tag, digest }, disabled });
+ digest | disabled | isDisabled
+ ${'foo'} | ${true} | ${'true'}
+ ${null} | ${true} | ${'true'}
+ ${null} | ${false} | ${undefined}
+ ${'foo'} | ${false} | ${undefined}
+ `(
+ 'disabled attribute is set to $isDisabled when the digest $digest and disabled is $disabled',
+ ({ digest, disabled, isDisabled }) => {
+ mountComponent({ tag: { ...tag, digest }, disabled });
- expect(findCheckbox().attributes('disabled')).toBe('true');
- });
+ expect(findCheckbox().attributes('disabled')).toBe(isDisabled);
+ },
+ );
it('is wired to the selected prop', () => {
mountComponent({ ...defaultProps, selected: true });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 56f12e2f0bb..0dcf988c814 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -1,16 +1,25 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stripTypenames } from 'helpers/graphql_helpers';
-import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
-import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/container_registry/explorer/constants/index';
+import {
+ GRAPHQL_PAGE_SIZE,
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ NO_TAGS_MATCHING_FILTERS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
+} from '~/packages_and_registries/container_registry/explorer/constants/index';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
const localVue = createLocalVue();
@@ -21,11 +30,20 @@ describe('Tags List', () => {
let resolver;
const tags = [...tagsMock];
+ const defaultConfig = {
+ noContainersImage: 'noContainersImage',
+ };
+
+ const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findTagsListRow = () => wrapper.findAllComponents(TagsListRow);
const findRegistryList = () => wrapper.findComponent(RegistryList);
- const findEmptyState = () => wrapper.findComponent(EmptyTagsState);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const fireFirstSortUpdate = () => {
+ findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
+ };
+
const waitForApolloRequestRender = async () => {
await waitForPromises();
await nextTick();
@@ -44,7 +62,7 @@ describe('Tags List', () => {
stubs: { RegistryList },
provide() {
return {
- config: {},
+ config: defaultConfig,
};
},
});
@@ -61,10 +79,23 @@ describe('Tags List', () => {
describe('registry list', () => {
beforeEach(() => {
mountComponent();
-
+ fireFirstSortUpdate();
return waitForApolloRequestRender();
});
+ it('has a persisted search', () => {
+ expect(findPersistedSearch().props()).toMatchObject({
+ defaultOrder: 'NAME',
+ defaultSort: 'asc',
+ sortableFields: [
+ {
+ label: 'Name',
+ orderBy: 'NAME',
+ },
+ ],
+ });
+ });
+
it('binds the correct props', () => {
expect(findRegistryList().props()).toMatchObject({
title: '2 tags',
@@ -75,11 +106,13 @@ describe('Tags List', () => {
});
describe('events', () => {
- it('prev-page fetch the previous page', () => {
+ it('prev-page fetch the previous page', async () => {
findRegistryList().vm.$emit('prev-page');
expect(resolver).toHaveBeenCalledWith({
first: null,
+ name: '',
+ sort: 'NAME_ASC',
before: tagsPageInfo.startCursor,
last: GRAPHQL_PAGE_SIZE,
id: '1',
@@ -92,6 +125,8 @@ describe('Tags List', () => {
expect(resolver).toHaveBeenCalledWith({
after: tagsPageInfo.endCursor,
first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
id: '1',
});
});
@@ -108,6 +143,7 @@ describe('Tags List', () => {
describe('list rows', () => {
it('one row exist for each tag', async () => {
mountComponent();
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -116,6 +152,7 @@ describe('Tags List', () => {
it('the correct props are bound to it', async () => {
mountComponent({ propsData: { disabled: true, id: 1 } });
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -130,7 +167,7 @@ describe('Tags List', () => {
describe('events', () => {
it('select event update the selected items', async () => {
mountComponent();
-
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
findTagsListRow().at(0).vm.$emit('select');
@@ -142,7 +179,7 @@ describe('Tags List', () => {
it('delete event emit a delete event', async () => {
mountComponent();
-
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
findTagsListRow().at(0).vm.$emit('delete');
@@ -154,32 +191,45 @@ describe('Tags List', () => {
describe('when the list of tags is empty', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(imageTagsMock([]));
- });
-
- it('has the empty state', async () => {
mountComponent();
-
- await waitForApolloRequestRender();
-
- expect(findEmptyState().exists()).toBe(true);
+ fireFirstSortUpdate();
+ return waitForApolloRequestRender();
});
- it('does not show the loader', async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
+ it('does not show the loader', () => {
expect(findTagsLoader().exists()).toBe(false);
});
- it('does not show the list', async () => {
- mountComponent();
+ it('does not show the list', () => {
+ expect(findRegistryList().exists()).toBe(false);
+ });
- await waitForApolloRequestRender();
+ describe('empty state', () => {
+ it('default empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultConfig.noContainersImage,
+ title: NO_TAGS_TITLE,
+ description: NO_TAGS_MESSAGE,
+ });
+ });
- expect(findRegistryList().exists()).toBe(false);
+ it('when filtered shows a filtered message', async () => {
+ findPersistedSearch().vm.$emit('update', {
+ sort: 'NAME_ASC',
+ filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'foo' } }],
+ });
+
+ await waitForApolloRequestRender();
+
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultConfig.noContainersImage,
+ title: NO_TAGS_MATCHING_FILTERS_TITLE,
+ description: NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
+ });
+ });
});
});
+
describe('loading state', () => {
it.each`
isImageLoading | queryExecuting | loadingVisible
@@ -191,7 +241,7 @@ describe('Tags List', () => {
'when the isImageLoading is $isImageLoading, and is $queryExecuting that the query is still executing is $loadingVisible that the loader is shown',
async ({ isImageLoading, queryExecuting, loadingVisible }) => {
mountComponent({ propsData: { isImageLoading, isMobile: false, id: 1 } });
-
+ fireFirstSortUpdate();
if (!queryExecuting) {
await waitForApolloRequestRender();
}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 9b821ba8ef3..7992bead60a 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -1,4 +1,4 @@
-import { GlKeysetPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlEmptyState } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
@@ -8,7 +8,6 @@ import axios from '~/lib/utils/axios_utils';
import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
import DeleteAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
import DetailsHeader from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
-import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
import PartialCleanupAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
import StatusAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
import TagsList from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
@@ -20,6 +19,8 @@ import {
ALERT_DANGER_IMAGE,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '~/packages_and_registries/container_registry/explorer/constants';
import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
@@ -50,7 +51,7 @@ describe('Details Page', () => {
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findEmptyState = () => wrapper.find(EmptyTagsState);
+ const findEmptyState = () => wrapper.find(GlEmptyState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
const findStatusAlert = () => wrapper.find(StatusAlert);
const findDeleteImage = () => wrapper.find(DeleteImage);
@@ -61,6 +62,10 @@ describe('Details Page', () => {
updateName: jest.fn(),
};
+ const defaultConfig = {
+ noContainersImage: 'noContainersImage',
+ };
+
const cleanTags = tagsMock.map((t) => {
const result = { ...t };
// eslint-disable-next-line no-underscore-dangle
@@ -78,7 +83,7 @@ describe('Details Page', () => {
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)),
options,
- config = {},
+ config = defaultConfig,
} = {}) => {
localVue.use(VueApollo);
@@ -154,7 +159,11 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
- expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props()).toMatchObject({
+ description: MISSING_OR_DELETED_IMAGE_MESSAGE,
+ svgPath: defaultConfig.noContainersImage,
+ title: MISSING_OR_DELETED_IMAGE_TITLE,
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
index 881d441e116..f95564e3fad 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = `
foo
<gl-button-stub
- aria-label="Copy this value"
+ aria-label="Copy SHA"
+ aria-live="polite"
buttontextclasses=""
category="tertiary"
+ data-clipboard-handle-tooltip="false"
data-clipboard-text="foo"
icon="copy-to-clipboard"
+ id="clipboard-button-1"
size="small"
title="Copy SHA"
variant="default"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
index 9ce590bfb51..d7caa8ca2d8 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/infrastructure_registry/details/c
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('FileSha', () => {
let wrapper;
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 99a7b8e427a..7cdf21dde46 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -10,10 +10,10 @@ exports[`packages_list_app renders 1`] = `
<div>
<section
- class="row empty-state text-center"
+ class="gl-display-flex empty-state gl-text-center gl-flex-direction-column"
>
<div
- class="col-12"
+ class="gl-max-w-full"
>
<div
class="svg-250 svg-content"
@@ -28,10 +28,10 @@ exports[`packages_list_app renders 1`] = `
</div>
<div
- class="col-12"
+ class="gl-max-w-full gl-m-auto"
>
<div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ class="gl-mx-auto gl-my-0 gl-p-5"
>
<h1
class="gl-font-size-h-display gl-line-height-36 h4"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index 2fb76b98925..26569f20e94 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -134,6 +134,8 @@ describe('packages_list', () => {
});
it('deleteItemConfirmation resets itemToBeDeleted', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemConfirmation();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
@@ -141,6 +143,8 @@ describe('packages_list', () => {
it('deleteItemConfirmation emit package:delete', () => {
const itemToBeDeleted = { id: 2 };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted });
wrapper.vm.deleteItemConfirmation();
return wrapper.vm.$nextTick(() => {
@@ -149,6 +153,8 @@ describe('packages_list', () => {
});
it('deleteItemCanceled resets itemToBeDeleted', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemCanceled();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
@@ -194,6 +200,8 @@ describe('packages_list', () => {
beforeEach(() => {
mountComponent();
eventSpy = jest.spyOn(Tracking, 'event');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
index e9f80d5f512..b3d0d88be4d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
@@ -23,14 +23,18 @@ exports[`ConanInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy Conan Setup Command"
- instruction="conan remote add gitlab conanPath"
+ instruction="conan remote add gitlab http://gdk.test:3000/api/v4/projects/1/packages/conan"
label="Add Conan Remote"
trackingaction="copy_conan_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the Conan registry,
+ <gl-link-stub
+ href="/help/user/packages/conan_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
index 881d441e116..f95564e3fad 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = `
foo
<gl-button-stub
- aria-label="Copy this value"
+ aria-label="Copy SHA"
+ aria-live="polite"
buttontextclasses=""
category="tertiary"
+ data-clipboard-handle-tooltip="false"
data-clipboard-text="foo"
icon="copy-to-clipboard"
+ id="clipboard-button-1"
size="small"
title="Copy SHA"
variant="default"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
index 4865b8205ab..67f1906f6fd 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
@@ -19,7 +19,7 @@ exports[`MavenInstallation groovy renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy add Gradle Groovy DSL repository command"
instruction="maven {
- url 'mavenPath'
+ url 'http://gdk.test:3000/api/v4/projects/1/packages/maven'
}"
label="Add Gradle Groovy DSL repository command"
multiline="true"
@@ -47,7 +47,7 @@ exports[`MavenInstallation kotlin renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy add Gradle Kotlin DSL repository command"
- instruction="maven(\\"mavenPath\\")"
+ instruction="maven(\\"http://gdk.test:3000/api/v4/projects/1/packages/maven\\")"
label="Add Gradle Kotlin DSL repository command"
multiline="true"
trackingaction="copy_kotlin_add_to_source_command"
@@ -64,9 +64,15 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
/>
<p>
- <gl-sprintf-stub
- message="Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
- />
+ Copy and paste this inside your
+ <code>
+ pom.xml
+ </code>
+
+ <code>
+ dependencies
+ </code>
+ block.
</p>
<code-instruction-stub
@@ -97,9 +103,11 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
</h3>
<p>
- <gl-sprintf-stub
- message="If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
- />
+ If you haven't already done so, you will need to add the below to your
+ <code>
+ pom.xml
+ </code>
+ file.
</p>
<code-instruction-stub
@@ -107,19 +115,19 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
instruction="<repositories>
<repository>
<id>gitlab-maven</id>
- <url>mavenPath</url>
+ <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
- <url>mavenPath</url>
+ <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
- <url>mavenPath</url>
+ <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
</snapshotRepository>
</distributionManagement>"
label=""
@@ -127,9 +135,13 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
trackingaction="copy_maven_setup_xml"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the Maven registry,
+ <gl-link-stub
+ href="/help/user/packages/maven_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
index d5649e39561..4520ae9c328 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
@@ -32,14 +32,18 @@ exports[`NpmInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy npm setup command"
- instruction="echo @gitlab-org:registry=npmPath/ >> .npmrc"
+ instruction="echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc"
label=""
trackingaction="copy_npm_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
- />
+ You may also need to setup authentication using an auth token.
+ <gl-link-stub
+ href="/help/user/packages/npm_registry/index"
+ target="_blank"
+ >
+ See the documentation
+ </gl-link-stub>
+ to find out more.
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
index 29ddd7b77ed..92930a6309a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
@@ -23,14 +23,18 @@ exports[`NugetInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy NuGet Setup Command"
- instruction="nuget source Add -Name \\"GitLab\\" -Source \\"nugetPath\\" -UserName <your_username> -Password <your_token>"
+ instruction="nuget source Add -Name \\"GitLab\\" -Source \\"http://gdk.test:3000/api/v4/projects/1/packages/nuget/index.json\\" -UserName <your_username> -Password <your_token>"
label="Add NuGet Source"
trackingaction="copy_nuget_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the NuGet registry,
+ <gl-link-stub
+ href="/help/user/packages/nuget_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 158bbbc3463..06ae8645101 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -10,7 +10,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy Pip command"
data-testid="pip-command"
- instruction="pip install @gitlab-org/package-15 --extra-index-url pypiPath"
+ instruction="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
label="Pip Command"
trackingaction="copy_pip_install_command"
trackinglabel="code_instruction"
@@ -23,16 +23,18 @@ exports[`PypiInstallation renders all the messages 1`] = `
</h3>
<p>
- <gl-sprintf-stub
- message="If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
- />
+ If you haven't already done so, you will need to add the below to your
+ <code>
+ .pypirc
+ </code>
+ file.
</p>
<code-instruction-stub
copytext="Copy .pypirc content"
data-testid="pypi-setup-content"
instruction="[gitlab]
-repository = pypiSetupPath
+repository = http://gdk.test:3000/api/v4/projects/1/packages/pypi
username = __token__
password = <your personal access token>"
label=""
@@ -40,9 +42,13 @@ password = <your personal access token>"
trackingaction="copy_pypi_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the PyPi registry,
+ <gl-link-stub
+ href="/help/user/packages/pypi_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
index aedf20e873a..0aba8f7efc7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
@@ -7,6 +7,7 @@ import {
TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
PACKAGE_TYPE_COMPOSER,
+ COMPOSER_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_COMPOSER };
@@ -24,9 +25,6 @@ describe('ComposerInstallation', () => {
function createComponent(groupListUrl = 'groupListUrl') {
wrapper = shallowMountExtended(ComposerInstallation, {
provide: {
- composerHelpPath: 'composerHelpPath',
- composerConfigRepositoryName: 'composerConfigRepositoryName',
- composerPath: 'composerPath',
groupListUrl,
},
propsData: { packageEntity },
@@ -61,7 +59,7 @@ describe('ComposerInstallation', () => {
const registryIncludeCommand = findRegistryInclude();
expect(registryIncludeCommand.exists()).toBe(true);
expect(registryIncludeCommand.props()).toMatchObject({
- instruction: `composer config repositories.composerConfigRepositoryName '{"type": "composer", "url": "composerPath"}'`,
+ instruction: `composer config repositories.${packageEntity.composerConfigRepositoryUrl} '{"type": "composer", "url": "${packageEntity.composerUrl}"}'`,
copyText: 'Copy registry include',
trackingAction: TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
});
@@ -96,7 +94,7 @@ describe('ComposerInstallation', () => {
'For more information on Composer packages in GitLab, see the documentation.',
);
expect(findHelpLink().attributes()).toMatchObject({
- href: 'composerHelpPath',
+ href: COMPOSER_HELP_PATH,
target: '_blank',
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
index 6b642cc21b7..bf9425def9a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
@@ -1,8 +1,12 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import ConanInstallation from '~/packages_and_registries/package_registry/components/details/conan_installation.vue';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
-import { PACKAGE_TYPE_CONAN } from '~/packages_and_registries/package_registry/constants';
+import {
+ PACKAGE_TYPE_CONAN,
+ CONAN_HELP_PATH,
+} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_CONAN };
@@ -12,16 +16,16 @@ describe('ConanInstallation', () => {
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent() {
wrapper = shallowMountExtended(ConanInstallation, {
- provide: {
- conanHelpPath: 'conanHelpPath',
- conanPath: 'conanPath',
- },
propsData: {
packageEntity,
},
+ stubs: {
+ GlSprintf,
+ },
});
}
@@ -58,8 +62,15 @@ describe('ConanInstallation', () => {
describe('setup commands', () => {
it('renders the correct command', () => {
expect(findCodeInstructions().at(1).props('instruction')).toBe(
- 'conan remote add gitlab conanPath',
+ `conan remote add gitlab ${packageEntity.conanUrl}`,
);
});
+
+ it('has a link to the docs', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: CONAN_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
index ebfbbe5b864..feed7a7c46c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/package_registry/components/detai
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('FileSha', () => {
let wrapper;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
index eed7e903833..fc60039db30 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -16,6 +17,7 @@ import {
TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND,
TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND,
PACKAGE_TYPE_MAVEN,
+ MAVEN_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -28,9 +30,6 @@ describe('MavenInstallation', () => {
metadata: mavenMetadata(),
};
- const mavenHelpPath = 'mavenHelpPath';
- const mavenPath = 'mavenPath';
-
const xmlCodeBlock = `<dependency>
<groupId>appGroup</groupId>
<artifactId>appName</artifactId>
@@ -40,43 +39,43 @@ describe('MavenInstallation', () => {
const mavenSetupXml = `<repositories>
<repository>
<id>gitlab-maven</id>
- <url>${mavenPath}</url>
+ <url>${packageEntity.mavenUrl}</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
- <url>${mavenPath}</url>
+ <url>${packageEntity.mavenUrl}</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
- <url>${mavenPath}</url>
+ <url>${packageEntity.mavenUrl}</url>
</snapshotRepository>
</distributionManagement>`;
const gradleGroovyInstallCommandText = `implementation 'appGroup:appName:appVersion'`;
const gradleGroovyAddSourceCommandText = `maven {
- url '${mavenPath}'
+ url '${packageEntity.mavenUrl}'
}`;
const gradleKotlinInstallCommandText = `implementation("appGroup:appName:appVersion")`;
- const gradleKotlinAddSourceCommandText = `maven("${mavenPath}")`;
+ const gradleKotlinAddSourceCommandText = `maven("${packageEntity.mavenUrl}")`;
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(MavenInstallation, {
- provide: {
- mavenHelpPath,
- mavenPath,
- },
propsData: {
packageEntity,
},
data() {
return data;
},
+ stubs: {
+ GlSprintf,
+ },
});
}
@@ -148,6 +147,13 @@ describe('MavenInstallation', () => {
trackingAction: TRACKING_ACTION_COPY_MAVEN_SETUP,
});
});
+
+ it('has a setup link', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: MAVEN_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index b89410ede13..8c0e2d948ca 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -1,4 +1,4 @@
-import { GlFormRadioGroup } from '@gitlab/ui';
+import { GlLink, GlSprintf, GlFormRadioGroup } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -15,6 +15,7 @@ import {
YARN_PACKAGE_MANAGER,
PROJECT_PACKAGE_ENDPOINT_TYPE,
INSTANCE_PACKAGE_ENDPOINT_TYPE,
+ NPM_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -29,13 +30,12 @@ describe('NpmInstallation', () => {
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
const findEndPointTypeSector = () => wrapper.findComponent(GlFormRadioGroup);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(NpmInstallation, {
provide: {
- npmHelpPath: 'npmHelpPath',
- npmPath: 'npmPath',
- npmProjectPath: 'npmProjectPath',
+ npmInstanceUrl: 'npmInstanceUrl',
},
propsData: {
packageEntity,
@@ -43,6 +43,7 @@ describe('NpmInstallation', () => {
data() {
return data;
},
+ stubs: { GlSprintf },
});
}
@@ -58,6 +59,13 @@ describe('NpmInstallation', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('has a setup link', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: NPM_HELP_PATH,
+ target: '_blank',
+ });
+ });
+
describe('endpoint type selector', () => {
it('has the endpoint type selector', () => {
expect(findEndPointTypeSector().exists()).toBe(true);
@@ -109,7 +117,7 @@ describe('NpmInstallation', () => {
it('renders the correct setup command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo @gitlab-org:registry=npmPath/ >> .npmrc',
+ instruction: 'echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -121,7 +129,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo @gitlab-org:registry=npmProjectPath/ >> .npmrc`,
+ instruction: `echo @gitlab-org:registry=${packageEntity.npmUrl}/ >> .npmrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -131,7 +139,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo @gitlab-org:registry=npmPath/ >> .npmrc`,
+ instruction: `echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -153,7 +161,7 @@ describe('NpmInstallation', () => {
it('renders the correct registry command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
@@ -165,7 +173,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo \\"@gitlab-org:registry\\" \\"npmProjectPath/\\" >> .yarnrc`,
+ instruction: `echo \\"@gitlab-org:registry\\" \\"${packageEntity.npmUrl}/\\" >> .yarnrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
@@ -175,7 +183,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
index c48a3f07299..d324d43258c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
@@ -6,6 +7,7 @@ import {
TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
PACKAGE_TYPE_NUGET,
+ NUGET_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -15,21 +17,18 @@ describe('NugetInstallation', () => {
let wrapper;
const nugetInstallationCommandStr = 'nuget install @gitlab-org/package-15 -Source "GitLab"';
- const nugetSetupCommandStr =
- 'nuget source Add -Name "GitLab" -Source "nugetPath" -UserName <your_username> -Password <your_token>';
+ const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${packageEntity.nugetUrl}" -UserName <your_username> -Password <your_token>`;
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent() {
wrapper = shallowMountExtended(NugetInstallation, {
- provide: {
- nugetHelpPath: 'nugetHelpPath',
- nugetPath: 'nugetPath',
- },
propsData: {
packageEntity,
},
+ stubs: { GlSprintf },
});
}
@@ -71,5 +70,12 @@ describe('NugetInstallation', () => {
trackingAction: TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
});
});
+
+ it('it has docs link', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: NUGET_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 042b2026199..f8a4ba8f3bc 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -28,8 +28,8 @@ describe('Package Files', () => {
const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => {
wrapper = mountExtended(PackageFiles, {
- provide: { canDelete },
propsData: {
+ canDelete,
packageFiles,
},
stubs: {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index 410c1b65348..f2fef6436a6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
@@ -6,6 +7,7 @@ import {
PACKAGE_TYPE_PYPI,
TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
+ PYPI_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI };
@@ -13,9 +15,9 @@ const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI };
describe('PypiInstallation', () => {
let wrapper;
- const pipCommandStr = 'pip install @gitlab-org/package-15 --extra-index-url pypiPath';
+ const pipCommandStr = `pip install @gitlab-org/package-15 --extra-index-url ${packageEntity.pypiUrl}`;
const pypiSetupStr = `[gitlab]
-repository = pypiSetupPath
+repository = ${packageEntity.pypiSetupUrl}
username = __token__
password = <your personal access token>`;
@@ -23,17 +25,16 @@ password = <your personal access token>`;
const setupInstruction = () => wrapper.findByTestId('pypi-setup-content');
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent() {
wrapper = shallowMountExtended(PypiInstallation, {
- provide: {
- pypiHelpPath: 'pypiHelpPath',
- pypiPath: 'pypiPath',
- pypiSetupPath: 'pypiSetupPath',
- },
propsData: {
packageEntity,
},
+ stubs: {
+ GlSprintf,
+ },
});
}
@@ -76,5 +77,12 @@ password = <your personal access token>`;
trackingAction: TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
});
});
+
+ it('has a link to the docs', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: PYPI_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 165ee962417..18a99f70756 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -22,16 +22,20 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
>
- <gl-link-stub
+ <router-link-stub
+ ariacurrentvalue="page"
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
- href="http://gdk.test:3000/gitlab-org/gitlab-test/-/packages/111"
+ data-testid="details-link"
+ event="click"
+ tag="a"
+ to="[object Object]"
>
<gl-truncate-stub
position="end"
text="@gitlab-org/package-15"
/>
- </gl-link-stub>
+ </router-link-stub>
<!---->
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 292667ec47c..9467a613b2a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,7 +1,11 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -13,6 +17,9 @@ import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+
describe('packages_list_row', () => {
let wrapper;
@@ -28,7 +35,7 @@ describe('packages_list_row', () => {
const findDeleteButton = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
const findListItem = () => wrapper.findComponent(ListItem);
- const findPackageLink = () => wrapper.findComponent(GlLink);
+ const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
@@ -40,6 +47,7 @@ describe('packages_list_row', () => {
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
+ localVue,
provide,
stubs: {
ListItem,
@@ -63,6 +71,15 @@ describe('packages_list_row', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('has a link to navigate to the details page', () => {
+ mountComponent();
+
+ expect(findPackageLink().props()).toMatchObject({
+ event: 'click',
+ to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
+ });
+ });
+
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
@@ -120,7 +137,7 @@ describe('packages_list_row', () => {
});
it('details link is disabled', () => {
- expect(findPackageLink().attributes('disabled')).toBe('true');
+ expect(findPackageLink().props('event')).toBe('');
});
it('has a warning icon', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 4c23b52b8a2..c6a59f20998 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -120,12 +120,22 @@ export const packageVersions = () => [
export const packageData = (extend) => ({
id: 'gid://gitlab/Packages::Package/111',
+ canDestroy: true,
name: '@gitlab-org/package-15',
packageType: 'NPM',
version: '1.0.0',
createdAt: '2020-08-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
status: 'DEFAULT',
+ mavenUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/maven',
+ npmUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/npm',
+ nugetUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/nuget/index.json',
+ composerConfigRepositoryUrl: 'gdk.test/22',
+ composerUrl: 'http://gdk.test:3000/api/v4/group/22/-/packages/composer/packages.json',
+ conanUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/conan',
+ pypiUrl:
+ 'http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple',
+ pypiSetupUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/pypi',
...extend,
});
@@ -185,6 +195,7 @@ export const packageDetailsQuery = (extendPackage) => ({
project: {
id: '1',
path: 'projectPath',
+ name: 'gitlab-test',
},
tags: {
nodes: packageTags(),
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
index dbe3c70c3cb..ed96abe24b1 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
@@ -11,10 +11,10 @@ exports[`PackagesListApp renders 1`] = `
<div>
<section
- class="row empty-state text-center"
+ class="gl-display-flex empty-state gl-text-center gl-flex-direction-column"
>
<div
- class="col-12"
+ class="gl-max-w-full"
>
<div
class="svg-250 svg-content"
@@ -29,10 +29,10 @@ exports[`PackagesListApp renders 1`] = `
</div>
<div
- class="col-12"
+ class="gl-max-w-full gl-m-auto"
>
<div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ class="gl-mx-auto gl-my-0 gl-p-5"
>
<h1
class="gl-font-size-h-display gl-line-height-36 h4"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 0bea84693f6..637e2edf3be 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -9,7 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
-import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
@@ -36,7 +36,7 @@ import {
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
-} from '../../mock_data';
+} from '../mock_data';
jest.mock('~/flash');
useMockLocationHelper();
@@ -47,21 +47,22 @@ describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ };
+
const provide = {
packageId: '111',
- titleComponent: 'PackageTitle',
- projectName: 'projectName',
- canDelete: 'canDelete',
- svgPath: 'svgPath',
- npmPath: 'npmPath',
- npmHelpPath: 'npmHelpPath',
+ emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
+ breadCrumbState,
};
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
+ routeId = '1',
} = {}) {
localVue.use(VueApollo);
@@ -87,6 +88,13 @@ describe('PackagesApp', () => {
GlTabs,
GlTab,
},
+ mocks: {
+ $route: {
+ params: {
+ id: routeId,
+ },
+ },
+ },
});
}
@@ -149,7 +157,7 @@ describe('PackagesApp', () => {
expect(findPackageHistory().exists()).toBe(true);
expect(findPackageHistory().props()).toMatchObject({
packageEntity: expect.objectContaining(packageData()),
- projectName: provide.projectName,
+ projectName: packageDetailsQuery().data.package.project.name,
});
});
@@ -175,9 +183,18 @@ describe('PackagesApp', () => {
});
});
+ it('calls the appropriate function to set the breadcrumbState', async () => {
+ const { name, version } = packageData();
+ createComponent();
+
+ await waitForPromises();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(`${name} v ${version}`);
+ });
+
describe('delete package', () => {
const originalReferrer = document.referrer;
- const setReferrer = (value = provide.projectName) => {
+ const setReferrer = (value = packageDetailsQuery().data.package.project.name) => {
Object.defineProperty(document, 'referrer', {
value,
configurable: true,
@@ -244,6 +261,7 @@ describe('PackagesApp', () => {
expect(findPackageFiles().exists()).toBe(true);
expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile);
+ expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy);
});
it('does not render the package files table when the package is composer', async () => {
diff --git a/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
index 5f243799bae..5f243799bae 100644
--- a/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 7044c1285d8..ceae8eebaef 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
-<div
+<nav
+ aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
@@ -24,19 +25,25 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class="gl-breadcrumb-separator"
data-testid="separator"
>
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="angle-right-icon"
- role="img"
+ <span
+ class="gl-mx-n5"
>
- <use
- href="#angle-right"
- />
- </svg>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s8"
+ data-testid="angle-right-icon"
+ role="img"
+ >
+ <use
+ href="#angle-right"
+ />
+ </svg>
+ </span>
</span>
</a>
</li>
+
+ <!---->
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -52,12 +59,15 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<!---->
</a>
</li>
+
+ <!---->
</ol>
-</div>
+</nav>
`;
exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
-<div
+<nav
+ aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
@@ -79,6 +89,8 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<!---->
</a>
</li>
+
+ <!---->
</ol>
-</div>
+</nav>
`;
diff --git a/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
index d6d1970cb12..d6d1970cb12 100644
--- a/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/package_path_spec.js b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
index 93425d4f399..93425d4f399 100644
--- a/spec/frontend/packages_and_registries/shared/package_path_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/package_tags_spec.js b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
index 33e96c0775e..33e96c0775e 100644
--- a/spec/frontend/packages_and_registries/shared/package_tags_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
index 0005162e0bb..0005162e0bb 100644
--- a/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
new file mode 100644
index 00000000000..bd492a5ae8f
--- /dev/null
+++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
@@ -0,0 +1,145 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import component from '~/packages_and_registries/shared/components/persisted_search.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+
+jest.mock('~/packages_and_registries/shared/utils');
+
+useMockLocationHelper();
+
+describe('Persisted Search', () => {
+ let wrapper;
+
+ const defaultQueryParamsMock = {
+ filters: ['foo'],
+ sorting: { sort: 'desc', orderBy: 'test' },
+ };
+
+ const defaultProps = {
+ sortableFields: [
+ { orderBy: 'test', label: 'test' },
+ { orderBy: 'foo', label: 'foo' },
+ ],
+ defaultOrder: 'test',
+ defaultSort: 'asc',
+ };
+
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+ const findUrlSync = () => wrapper.findComponent(UrlSync);
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ stubs: {
+ UrlSync,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has a registry search component', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(findRegistrySearch().exists()).toBe(true);
+ });
+
+ it('registry search is mounted after mount', async () => {
+ mountComponent();
+
+ expect(findRegistrySearch().exists()).toBe(false);
+ });
+
+ it('has a UrlSync component', () => {
+ mountComponent();
+
+ expect(findUrlSync().exists()).toBe(true);
+ });
+
+ it('on sorting:changed emits update event and update internal sort', async () => {
+ const payload = { sort: 'desc', orderBy: 'test' };
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('sorting:changed', payload);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject(payload);
+
+ // there is always a first call on mounted that emits up default values
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: ['foo'],
+ sort: 'TEST_DESC',
+ },
+ ]);
+ });
+
+ it('on filter:changed updates the filters', async () => {
+ const payload = ['foo'];
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('filter:changed', payload);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props('filter')).toEqual(['foo']);
+ });
+
+ it('on filter:submit emits update event', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: ['foo'],
+ sort: 'TEST_DESC',
+ },
+ ]);
+ });
+
+ it('on query:changed calls updateQuery from UrlSync', async () => {
+ jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('query:changed');
+
+ expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
+ });
+
+ it('sets the component sorting and filtering based on the querystring', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(getQueryParams).toHaveBeenCalled();
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: defaultQueryParamsMock.filters,
+ sorting: defaultQueryParamsMock.sorting,
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/shared/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
index fa8f8f7641a..fa8f8f7641a 100644
--- a/spec/frontend/packages_and_registries/shared/publish_method_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
index e5a8438f23f..6dfe116c285 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import component from '~/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue';
+import component from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
describe('Registry Breadcrumb', () => {
let wrapper;
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 5bba98bdf96..6a7ce80ec5a 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -94,13 +94,13 @@ describe('Todos', () => {
});
it('updates pending text', () => {
- expect(document.querySelector('.js-todos-pending .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
addDelimiter(TEST_COUNT_BIG),
);
});
it('updates done text', () => {
- expect(document.querySelector('.js-todos-done .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
addDelimiter(TEST_DONE_COUNT_BIG),
);
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index 53c1733eab9..b700c255e8c 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -38,14 +38,14 @@ describe('Timezone Dropdown', () => {
const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
const tzValue = 'Asia/Colombo';
- expect($inputEl.val()).toBe('UTC');
+ expect($inputEl.val()).toBe('Etc/UTC');
$(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click');
const val = $inputEl.val();
expect(val).toBe(tzValue);
- expect(val).not.toBe('UTC');
+ expect(val).not.toBe('Etc/UTC');
});
it('will format data array of timezones into a list of offsets', () => {
@@ -67,7 +67,7 @@ describe('Timezone Dropdown', () => {
it('will default the timezone to UTC', () => {
const tz = $inputEl.val();
- expect(tz).toBe('UTC');
+ expect(tz).toBe('Etc/UTC');
});
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 0020269e4e7..8a9bb025d55 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -7,6 +7,7 @@ import {
visibilityLevelDescriptions,
visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
const defaultProps = {
currentSettings: {
@@ -47,6 +48,8 @@ const defaultProps = {
packagesAvailable: false,
packagesHelpPath: '/help/user/packages/index',
requestCveAvailable: true,
+ confirmationPhrase: 'my-fake-project',
+ showVisibilityConfirmModal: false,
};
describe('Settings Panel', () => {
@@ -104,6 +107,7 @@ describe('Settings Panel', () => {
);
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
+ const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
afterEach(() => {
wrapper.destroy();
@@ -177,6 +181,44 @@ describe('Settings Panel', () => {
expect(findRequestAccessEnabledInput().exists()).toBe(false);
});
+
+ it('does not require confirmation if the visibility is reduced', async () => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ });
+
+ expect(findConfirmDangerButton().exists()).toBe(false);
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findConfirmDangerButton().exists()).toBe(false);
+ });
+
+ describe('showVisibilityConfirmModal=true', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ showVisibilityConfirmModal: true,
+ });
+ });
+
+ it('will render the confirmation dialog if the visibility is reduced', async () => {
+ expect(findConfirmDangerButton().exists()).toBe(false);
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findConfirmDangerButton().exists()).toBe(true);
+ });
+
+ it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findConfirmDangerButton().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).toHaveLength(1);
+ });
+ });
});
describe('Issues settings', () => {
diff --git a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
index 2c8eb8e459f..04f53e048ed 100644
--- a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
+++ b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
@@ -57,9 +57,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('is-over', 'is-showing-fly-out');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Fly out',
}),
@@ -74,9 +74,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
submenuList.classList.add('fly-out-list');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu_item',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu_item',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Fly out',
}),
@@ -92,9 +92,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('active');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Expanded',
}),
@@ -108,9 +108,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('active');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu_item',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu_item',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Expanded',
}),
@@ -131,9 +131,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('is-over', 'is-showing-fly-out');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu',
+ extra: JSON.stringify({
sidebar_display: 'Collapsed',
menu_display: 'Fly out',
}),
@@ -148,9 +148,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
submenuList.classList.add('fly-out-list');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu_item',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu_item',
+ extra: JSON.stringify({
sidebar_display: 'Collapsed',
menu_display: 'Fly out',
}),
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index f4236146d33..fd581eebd1e 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal, GlAlert, GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -31,25 +31,28 @@ describe('WikiForm', () => {
const findContent = () => wrapper.find('#wiki_content');
const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
- const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
- const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use the new editor' });
+ const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
+ const findUseNewEditorButton = () => wrapper.findByText('Use the new editor');
const findToggleEditingModeButton = () => wrapper.findByTestId('toggle-editing-mode-button');
- const findDismissContentEditorAlertButton = () =>
- wrapper.findByRole('button', { name: 'Try this later' });
+ const findDismissContentEditorAlertButton = () => wrapper.findByText('Try this later');
const findSwitchToOldEditorButton = () =>
wrapper.findByRole('button', { name: 'Switch me back to the classic editor.' });
- const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'Learn more.' });
+ const findTitleHelpLink = () => wrapper.findByText('Learn more.');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
const findContentEditor = () => wrapper.findComponent(ContentEditor);
const findClassicEditor = () => wrapper.findComponent(MarkdownField);
const setFormat = (value) => {
const format = findFormat();
- format.find(`option[value=${value}]`).setSelected();
- format.element.dispatchEvent(new Event('change'));
+
+ return format.find(`option[value=${value}]`).setSelected();
};
- const triggerFormSubmit = () => findForm().element.dispatchEvent(new Event('submit'));
+ const triggerFormSubmit = () => {
+ findForm().element.dispatchEvent(new Event('submit'));
+
+ return nextTick();
+ };
const dispatchBeforeUnload = () => {
const e = new Event('beforeunload');
@@ -84,34 +87,14 @@ describe('WikiForm', () => {
Org: 'org',
};
- function createWrapper(
- persisted = false,
- { pageInfo, glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false } } = {},
- ) {
- wrapper = extendedWrapper(
- mount(
- WikiForm,
- {
- provide: {
- formatOptions,
- glFeatures,
- pageInfo: {
- ...(persisted ? pageInfoPersisted : pageInfoNew),
- ...pageInfo,
- },
- },
- },
- { attachToDocument: true },
- ),
- );
- }
-
- const createShallowWrapper = (
+ function createWrapper({
+ mountFn = shallowMount,
persisted = false,
- { pageInfo, glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false } } = {},
- ) => {
+ pageInfo,
+ glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false },
+ } = {}) {
wrapper = extendedWrapper(
- shallowMount(WikiForm, {
+ mountFn(WikiForm, {
provide: {
formatOptions,
glFeatures,
@@ -122,10 +105,12 @@ describe('WikiForm', () => {
},
stubs: {
MarkdownField,
+ GlAlert,
+ GlButton,
},
}),
);
- };
+ }
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
@@ -147,26 +132,24 @@ describe('WikiForm', () => {
`(
'updates the commit message to $message when title is $title and persisted=$persisted',
async ({ title, message, persisted }) => {
- createWrapper(persisted);
-
- findTitle().setValue(title);
+ createWrapper({ persisted });
- await wrapper.vm.$nextTick();
+ await findTitle().setValue(title);
expect(findMessage().element.value).toBe(message);
},
);
it('sets the commit message to "Update My page" when the page first loads when persisted', async () => {
- createWrapper(true);
+ createWrapper({ persisted: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findMessage().element.value).toBe('Update My page');
});
it('does not trim page content by default', () => {
- createWrapper(true);
+ createWrapper({ persisted: true });
expect(findContent().element.value).toBe(' My page content ');
});
@@ -178,20 +161,16 @@ describe('WikiForm', () => {
${'asciidoc'} | ${'link:page-slug[Link title]'}
${'org'} | ${'[[page-slug]]'}
`('updates the link help message when format=$value is selected', async ({ value, text }) => {
- createWrapper();
+ createWrapper({ mountFn: mount });
- setFormat(value);
-
- await wrapper.vm.$nextTick();
+ await setFormat(value);
expect(wrapper.text()).toContain(text);
});
- it('starts with no unload warning', async () => {
+ it('starts with no unload warning', () => {
createWrapper();
- await wrapper.vm.$nextTick();
-
const e = dispatchBeforeUnload();
expect(typeof e.returnValue).not.toBe('string');
expect(e.preventDefault).not.toHaveBeenCalled();
@@ -203,20 +182,16 @@ describe('WikiForm', () => {
${false} | ${'You can specify the full path for the new file. We will automatically create any missing directories.'} | ${'/help/user/project/wiki/index#create-a-new-wiki-page'}
`(
'shows appropriate title help text and help link for when persisted=$persisted',
- async ({ persisted, titleHelpLink, titleHelpText }) => {
- createWrapper(persisted);
-
- await wrapper.vm.$nextTick();
+ ({ persisted, titleHelpLink, titleHelpText }) => {
+ createWrapper({ persisted });
expect(wrapper.text()).toContain(titleHelpText);
expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
},
);
- it('shows correct link for wiki specific markdown docs', async () => {
- createWrapper();
-
- await wrapper.vm.$nextTick();
+ it('shows correct link for wiki specific markdown docs', () => {
+ createWrapper({ mountFn: mount });
expect(findMarkdownHelpLink().attributes().href).toBe(
'/help/user/markdown#wiki-specific-markdown',
@@ -225,12 +200,11 @@ describe('WikiForm', () => {
describe('when wiki content is updated', () => {
beforeEach(async () => {
- createWrapper(true);
+ createWrapper({ mountFn: mount, persisted: true });
const input = findContent();
- input.setValue(' Lorem ipsum dolar sit! ');
- await input.trigger('input');
+ await input.setValue(' Lorem ipsum dolar sit! ');
});
it('sets before unload warning', () => {
@@ -241,17 +215,15 @@ describe('WikiForm', () => {
describe('form submit', () => {
beforeEach(async () => {
- triggerFormSubmit();
-
- await wrapper.vm.$nextTick();
+ await triggerFormSubmit();
});
- it('when form submitted, unsets before unload warning', async () => {
+ it('when form submitted, unsets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
- it('triggers wiki format tracking event', async () => {
+ it('triggers wiki format tracking event', () => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
@@ -264,22 +236,20 @@ describe('WikiForm', () => {
describe('submit button state', () => {
it.each`
title | content | buttonState | disabledAttr
- ${'something'} | ${'something'} | ${'enabled'} | ${undefined}
- ${''} | ${'something'} | ${'disabled'} | ${'disabled'}
- ${'something'} | ${''} | ${'disabled'} | ${'disabled'}
- ${''} | ${''} | ${'disabled'} | ${'disabled'}
- ${' '} | ${' '} | ${'disabled'} | ${'disabled'}
+ ${'something'} | ${'something'} | ${'enabled'} | ${false}
+ ${''} | ${'something'} | ${'disabled'} | ${true}
+ ${'something'} | ${''} | ${'disabled'} | ${true}
+ ${''} | ${''} | ${'disabled'} | ${true}
+ ${' '} | ${' '} | ${'disabled'} | ${true}
`(
"when title='$title', content='$content', then the button is $buttonState'",
async ({ title, content, disabledAttr }) => {
createWrapper();
- findTitle().setValue(title);
- findContent().setValue(content);
+ await findTitle().setValue(title);
+ await findContent().setValue(content);
- await wrapper.vm.$nextTick();
-
- expect(findSubmitButton().attributes().disabled).toBe(disabledAttr);
+ expect(findSubmitButton().props().disabled).toBe(disabledAttr);
},
);
@@ -288,7 +258,7 @@ describe('WikiForm', () => {
${true} | ${'Save changes'}
${false} | ${'Create page'}
`('when persisted=$persisted, label is set to $buttonLabel', ({ persisted, buttonLabel }) => {
- createWrapper(persisted);
+ createWrapper({ persisted });
expect(findSubmitButton().text()).toBe(buttonLabel);
});
@@ -302,7 +272,7 @@ describe('WikiForm', () => {
`(
'when persisted=$persisted, redirects the user to appropriate path',
({ persisted, redirectLink }) => {
- createWrapper(persisted);
+ createWrapper({ persisted });
expect(findCancelButton().attributes().href).toBe(redirectLink);
},
@@ -311,7 +281,7 @@ describe('WikiForm', () => {
describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is not enabled', () => {
beforeEach(() => {
- createShallowWrapper(true, {
+ createWrapper({
glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: false },
});
});
@@ -323,7 +293,7 @@ describe('WikiForm', () => {
describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is enabled', () => {
beforeEach(() => {
- createShallowWrapper(true, {
+ createWrapper({
glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: true },
});
});
@@ -404,10 +374,6 @@ describe('WikiForm', () => {
});
describe('wiki content editor', () => {
- beforeEach(() => {
- createWrapper(true);
- });
-
it.each`
format | buttonExists
${'markdown'} | ${true}
@@ -415,15 +381,17 @@ describe('WikiForm', () => {
`(
'gl-alert containing "use new editor" button exists: $buttonExists if format is $format',
async ({ format, buttonExists }) => {
- setFormat(format);
+ createWrapper();
- await wrapper.vm.$nextTick();
+ await setFormat(format);
expect(findUseNewEditorButton().exists()).toBe(buttonExists);
},
);
it('gl-alert containing "use new editor" button is dismissed on clicking dismiss button', async () => {
+ createWrapper();
+
await findDismissContentEditorAlertButton().trigger('click');
expect(findUseNewEditorButton().exists()).toBe(false);
@@ -442,22 +410,24 @@ describe('WikiForm', () => {
);
};
- it('shows classic editor by default', assertOldEditorIsVisible);
+ it('shows classic editor by default', () => {
+ createWrapper({ persisted: true });
+
+ assertOldEditorIsVisible();
+ });
describe('switch format to rdoc', () => {
beforeEach(async () => {
- setFormat('rdoc');
+ createWrapper({ persisted: true });
- await wrapper.vm.$nextTick();
+ await setFormat('rdoc');
});
it('continues to show the classic editor', assertOldEditorIsVisible);
describe('switch format back to markdown', () => {
beforeEach(async () => {
- setFormat('rdoc');
-
- await wrapper.vm.$nextTick();
+ await setFormat('markdown');
});
it(
@@ -469,6 +439,7 @@ describe('WikiForm', () => {
describe('clicking "use new editor": editor fails to load', () => {
beforeEach(async () => {
+ createWrapper({ mountFn: mount });
mock.onPost(/preview-markdown/).reply(400);
await findUseNewEditorButton().trigger('click');
@@ -494,10 +465,12 @@ describe('WikiForm', () => {
});
describe('clicking "use new editor": editor loads successfully', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ createWrapper({ persisted: true, mountFn: mount });
+
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
- findUseNewEditorButton().trigger('click');
+ await findUseNewEditorButton().trigger('click');
});
it('shows a tip to send feedback', () => {
@@ -542,46 +515,40 @@ describe('WikiForm', () => {
});
it('unsets before unload warning on form submit', async () => {
- triggerFormSubmit();
-
- await nextTick();
+ await triggerFormSubmit();
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
- });
- it('triggers tracking events on form submit', async () => {
- triggerFormSubmit();
+ it('triggers tracking events on form submit', async () => {
+ await triggerFormSubmit();
- await wrapper.vm.$nextTick();
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
- label: WIKI_FORMAT_LABEL,
- extra: {
- value: findFormat().element.value,
- old_format: pageInfoPersisted.format,
- project_path: pageInfoPersisted.path,
- },
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ extra: {
+ value: findFormat().element.value,
+ old_format: pageInfoPersisted.format,
+ project_path: pageInfoPersisted.path,
+ },
+ });
});
- });
-
- it('updates content from content editor on form submit', async () => {
- // old value
- expect(findContent().element.value).toBe(' My page content ');
- // wait for content editor to load
- await waitForPromises();
+ it('updates content from content editor on form submit', async () => {
+ // old value
+ expect(findContent().element.value).toBe(' My page content ');
- triggerFormSubmit();
+ // wait for content editor to load
+ await waitForPromises();
- await wrapper.vm.$nextTick();
+ await triggerFormSubmit();
- expect(findContent().element.value).toBe('hello **world**');
+ expect(findContent().element.value).toBe('hello **world**');
+ });
});
describe('clicking "switch to classic editor"', () => {
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
index cab4810cbf1..f15d5f334d6 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -17,19 +17,12 @@ describe('Pipeline Editor | Text editor component', () => {
let editorReadyListener;
let mockUse;
let mockRegisterCiSchema;
+ let mockEditorInstance;
+ let editorInstanceDetail;
const MockSourceEditor = {
template: '<div/>',
props: ['value', 'fileName'],
- mounted() {
- this.$emit(EDITOR_READY_EVENT);
- },
- methods: {
- getEditor: () => ({
- use: mockUse,
- registerCiSchema: mockRegisterCiSchema,
- }),
- },
};
const createComponent = (glFeatures = {}, mountFn = shallowMount) => {
@@ -58,6 +51,21 @@ describe('Pipeline Editor | Text editor component', () => {
const findEditor = () => wrapper.findComponent(MockSourceEditor);
+ beforeEach(() => {
+ editorReadyListener = jest.fn();
+ mockUse = jest.fn();
+ mockRegisterCiSchema = jest.fn();
+ mockEditorInstance = {
+ use: mockUse,
+ registerCiSchema: mockRegisterCiSchema,
+ };
+ editorInstanceDetail = {
+ detail: {
+ instance: mockEditorInstance,
+ },
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
@@ -67,10 +75,6 @@ describe('Pipeline Editor | Text editor component', () => {
describe('template', () => {
beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
-
createComponent();
});
@@ -87,7 +91,7 @@ describe('Pipeline Editor | Text editor component', () => {
});
it('bubbles up events', () => {
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
expect(editorReadyListener).toHaveBeenCalled();
});
@@ -97,11 +101,7 @@ describe('Pipeline Editor | Text editor component', () => {
describe('when `schema_linting` feature flag is on', () => {
beforeEach(() => {
createComponent({ schemaLinting: true });
- // Since the editor will have already mounted, the event will have fired.
- // To ensure we properly test this, we clear the mock and re-remit the event.
- mockRegisterCiSchema.mockClear();
- mockUse.mockClear();
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('configures editor with syntax highlight', () => {
@@ -113,7 +113,7 @@ describe('Pipeline Editor | Text editor component', () => {
describe('when `schema_linting` feature flag is off', () => {
beforeEach(() => {
createComponent();
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('does not call the register CI schema function', () => {
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index fd8a100bb2c..570323826d1 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,40 +1,61 @@
+import VueApollo from 'vue-apollo';
import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { escape } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
+ EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
-import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
+import {
+ mergeUnwrappedCiConfig,
+ mockCiYml,
+ mockLintUnavailableHelpPagePath,
+ mockYmlHelpPagePath,
+} from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Validation segment component', () => {
let wrapper;
- const createComponent = ({ props = {}, appStatus }) => {
+ const mockApollo = createMockApollo();
+
+ const createComponent = ({ props = {}, appStatus = EDITOR_APP_STATUS_INVALID }) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: appStatus,
+ },
+ },
+ });
+
wrapper = extendedWrapper(
shallowMount(ValidationSegment, {
+ localVue,
+ apolloProvider: mockApollo,
provide: {
ymlHelpPagePath: mockYmlHelpPagePath,
+ lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
},
propsData: {
ciConfig: mergeUnwrappedCiConfig(),
ciFileContent: mockCiYml,
...props,
},
- // Simulate graphQL client query result
- data() {
- return {
- appStatus,
- };
- },
}),
);
};
@@ -92,6 +113,7 @@ describe('Validation segment component', () => {
appStatus: EDITOR_APP_STATUS_INVALID,
});
});
+
it('has warning icon', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
@@ -149,4 +171,28 @@ describe('Validation segment component', () => {
});
});
});
+
+ describe('when the lint service is unavailable', () => {
+ beforeEach(() => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_LINT_UNAVAILABLE,
+ props: {
+ ciConfig: {},
+ },
+ });
+ });
+
+ it('show a message that the service is unavailable', () => {
+ expect(findValidationMsg().text()).toBe(i18n.unavailableValidation);
+ });
+
+ it('shows the time-out icon', () => {
+ expect(findIcon().props('name')).toBe('time-out');
+ });
+
+ it('shows the learn more link', () => {
+ expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath);
+ expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
index 3becf82ed6e..6206a0f6aed 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
@@ -75,34 +75,83 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
expect(mockChildMounted).toHaveBeenCalledWith(mockContent1);
});
- describe('showing the tab content depending on `isEmpty` and `isInvalid`', () => {
+ describe('alerts', () => {
+ describe('unavailable state', () => {
+ beforeEach(() => {
+ createWrapper({ props: { isUnavailable: true } });
+ });
+
+ it('shows the invalid alert when the status is invalid', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toContain(wrapper.vm.$options.i18n.unavailable);
+ });
+ });
+
+ describe('invalid state', () => {
+ beforeEach(() => {
+ createWrapper({ props: { isInvalid: true } });
+ });
+
+ it('shows the invalid alert when the status is invalid', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(wrapper.vm.$options.i18n.invalid);
+ });
+ });
+
+ describe('empty state', () => {
+ const text = 'my custom alert message';
+
+ beforeEach(() => {
+ createWrapper({
+ props: { isEmpty: true, emptyMessage: text },
+ });
+ });
+
+ it('displays an empty message', () => {
+ createWrapper({
+ props: { isEmpty: true },
+ });
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(
+ 'This tab will be usable when the CI/CD configuration file is populated with valid syntax.',
+ );
+ });
+
+ it('can have a custom empty message', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(text);
+ });
+ });
+ });
+
+ describe('showing the tab content depending on `isEmpty`, `isUnavailable` and `isInvalid`', () => {
it.each`
- isEmpty | isInvalid | showSlotComponent | text
- ${undefined} | ${undefined} | ${true} | ${'renders'}
- ${false} | ${false} | ${true} | ${'renders'}
- ${undefined} | ${true} | ${false} | ${'hides'}
- ${true} | ${false} | ${false} | ${'hides'}
- ${false} | ${true} | ${false} | ${'hides'}
+ isEmpty | isUnavailable | isInvalid | showSlotComponent | text
+ ${undefined} | ${undefined} | ${undefined} | ${true} | ${'renders'}
+ ${false} | ${false} | ${false} | ${true} | ${'renders'}
+ ${undefined} | ${true} | ${true} | ${false} | ${'hides'}
+ ${true} | ${false} | ${false} | ${false} | ${'hides'}
+ ${false} | ${true} | ${false} | ${false} | ${'hides'}
+ ${false} | ${false} | ${true} | ${false} | ${'hides'}
`(
- '$text the slot component when isEmpty:$isEmpty and isInvalid:$isInvalid',
- ({ isEmpty, isInvalid, showSlotComponent }) => {
+ '$text the slot component when isEmpty:$isEmpty, isUnavailable:$isUnavailable and isInvalid:$isInvalid',
+ ({ isEmpty, isUnavailable, isInvalid, showSlotComponent }) => {
createWrapper({
- props: { isEmpty, isInvalid },
+ props: { isEmpty, isUnavailable, isInvalid },
});
expect(findSlotComponent().exists()).toBe(showSlotComponent);
expect(findAlert().exists()).toBe(!showSlotComponent);
},
);
-
- it('can have a custom empty message', () => {
- const text = 'my custom alert message';
- createWrapper({ props: { isEmpty: true, emptyMessage: text } });
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
- expect(alert.text()).toBe(text);
- });
});
describe('user interaction', () => {
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index fc2cbdeda0a..f02f6870653 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -10,6 +10,7 @@ export const mockNewMergeRequestPath = '/-/merge_requests/new';
export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockLintHelpPagePath = '/-/lint-help';
+export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
export const mockYmlHelpPagePath = '/-/yml-help';
export const mockCommitMessage = 'My commit message';
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 09d7d4f7ca6..63eca253c48 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -5,10 +5,15 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
-import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
+import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import ValidationSegment, {
+ i18n as validationSegmenti18n,
+} from '~/pipeline_editor/components/header/validation_segment.vue';
+import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
@@ -61,11 +66,6 @@ describe('Pipeline editor app component', () => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide },
stubs,
- data() {
- return {
- commitSha: '',
- };
- },
mocks: {
$apollo: {
queries: {
@@ -90,17 +90,11 @@ describe('Pipeline editor app component', () => {
[getLatestCommitShaQuery, mockLatestCommitShaQuery],
[getPipelineQuery, mockPipelineQuery],
];
- mockApollo = createMockApollo(handlers);
+
+ mockApollo = createMockApollo(handlers, resolvers);
const options = {
localVue,
- data() {
- return {
- currentBranch: mockDefaultBranch,
- lastCommitBranch: '',
- appStatus: '',
- };
- },
mocks: {},
apolloProvider: mockApollo,
};
@@ -116,6 +110,7 @@ describe('Pipeline editor app component', () => {
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
+ const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
beforeEach(() => {
mockBlobContentData = jest.fn();
@@ -240,6 +235,26 @@ describe('Pipeline editor app component', () => {
});
});
+ describe('when the lint query returns a 500 error', () => {
+ beforeEach(async () => {
+ mockCiConfigData.mockRejectedValueOnce(new Error(500));
+ await createComponentWithApollo({
+ stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
+ });
+ });
+
+ it('shows that the lint service is down', () => {
+ expect(findValidationSegment().text()).toContain(
+ validationSegmenti18n.unavailableValidation,
+ );
+ });
+
+ it('does not report an error or scroll to the top', () => {
+ expect(findAlert().exists()).toBe(false);
+ expect(window.scrollTo).not.toHaveBeenCalled();
+ });
+ });
+
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
const updateSuccessMessage = 'Your changes have been successfully committed.';
@@ -411,94 +426,6 @@ describe('Pipeline editor app component', () => {
});
});
- describe('when multiple errors occurs in a row', () => {
- const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
- const unknownFailureMessage = 'The CI configuration was not loaded, please try again.';
- const unknownReasons = ['Commit failed'];
- const alertErrorMessage = `${updateFailureMessage} ${unknownReasons[0]}`;
-
- const emitError = (type = COMMIT_FAILURE, reasons = unknownReasons) =>
- findEditorHome().vm.$emit('showError', {
- type,
- reasons,
- });
-
- beforeEach(async () => {
- mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
- mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
- mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
-
- window.scrollTo = jest.fn();
-
- await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
- await emitError();
- });
-
- it('shows an error message for the first error', () => {
- expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
- });
-
- it('scrolls to the top of the page to bring attention to the error message', () => {
- expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
- expect(window.scrollTo).toHaveBeenCalledTimes(1);
- });
-
- it('does not scroll to the top of the page if the same error occur multiple times in a row', async () => {
- await emitError();
-
- expect(window.scrollTo).toHaveBeenCalledTimes(1);
- expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
- });
-
- it('scrolls to the top if the error is different', async () => {
- await emitError(LOAD_FAILURE_UNKNOWN, []);
-
- expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- });
-
- describe('when a user dismiss the alert', () => {
- beforeEach(async () => {
- await findAlert().vm.$emit('dismiss');
- });
-
- it('shows an error if the type is the same, but the reason is different', async () => {
- const newReason = 'Something broke';
-
- await emitError(COMMIT_FAILURE, [newReason]);
-
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- expect(findAlert().text()).toMatchInterpolatedText(`${updateFailureMessage} ${newReason}`);
- });
-
- it('does not show an error or scroll if a new error with the same type occurs', async () => {
- await emitError();
-
- expect(window.scrollTo).toHaveBeenCalledTimes(1);
- expect(findAlert().exists()).toBe(false);
- });
-
- it('it shows an error and scroll when a new type is emitted', async () => {
- await emitError(LOAD_FAILURE_UNKNOWN, []);
-
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
- });
-
- it('it shows an error and scroll if a previously shown type happen again', async () => {
- await emitError(LOAD_FAILURE_UNKNOWN, []);
-
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
-
- await emitError();
-
- expect(window.scrollTo).toHaveBeenCalledTimes(3);
- expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
- });
- });
- });
-
describe('when add_new_config_file query param is present', () => {
const originalLocation = window.location.href;
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
index 99de0d2a3ef..52461885342 100644
--- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
@@ -13,6 +13,7 @@ Array [
"id": "6",
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -53,6 +54,7 @@ Array [
"id": "11",
"name": "build_b",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -93,6 +95,7 @@ Array [
"id": "16",
"name": "build_c",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -133,6 +136,7 @@ Array [
"id": "21",
"name": "build_d 1/3",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -157,6 +161,7 @@ Array [
"id": "24",
"name": "build_d 2/3",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -181,6 +186,7 @@ Array [
"id": "27",
"name": "build_d 3/3",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -221,6 +227,7 @@ Array [
"id": "59",
"name": "test_c",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -267,6 +274,11 @@ Array [
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_c",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -313,6 +325,13 @@ Array [
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_d 3/3",
+ "build_d 2/3",
+ "build_d 1/3",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -343,6 +362,13 @@ Array [
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_d 3/3",
+ "build_d 2/3",
+ "build_d 1/3",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -385,6 +411,9 @@ Array [
"needs": Array [
"build_b",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_b",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index dcbbde7bf36..41823bfdb9f 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -73,6 +73,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -118,6 +122,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -163,6 +171,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -208,6 +220,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
{
__typename: 'CiJob',
@@ -235,6 +251,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
{
__typename: 'CiJob',
@@ -262,6 +282,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -339,6 +363,27 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '37',
+ name: 'build_c',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '38',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '39',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
},
],
},
@@ -411,6 +456,37 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '45',
+ name: 'build_d 3/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '46',
+ name: 'build_d 2/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '47',
+ name: 'build_d 1/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '48',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '49',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
},
{
__typename: 'CiJob',
@@ -465,6 +541,37 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '52',
+ name: 'build_d 3/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '53',
+ name: 'build_d 2/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '54',
+ name: 'build_d 1/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '55',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '56',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
},
],
},
@@ -503,6 +610,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -547,6 +658,16 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '65',
+ name: 'build_b',
+ },
+ ],
+ },
},
],
},
@@ -720,6 +841,10 @@ export const wrappedPipelineReturn = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
status: {
__typename: 'DetailedStatus',
id: '84',
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index 42adefcd0bb..bda07af4feb 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -79,6 +79,8 @@ describe('UpdateUsername component', () => {
beforeEach(async () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsername });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/profile/add_ssh_key_validation_spec.js b/spec/frontend/profile/add_ssh_key_validation_spec.js
index 1fec864599c..a6bcca0ccb3 100644
--- a/spec/frontend/profile/add_ssh_key_validation_spec.js
+++ b/spec/frontend/profile/add_ssh_key_validation_spec.js
@@ -3,18 +3,18 @@ import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh
describe('AddSshKeyValidation', () => {
describe('submit', () => {
it('returns true if isValid is true', () => {
- const addSshKeyValidation = new AddSshKeyValidation({});
- jest.spyOn(AddSshKeyValidation, 'isPublicKey').mockReturnValue(true);
+ const addSshKeyValidation = new AddSshKeyValidation([], {});
+ jest.spyOn(addSshKeyValidation, 'isPublicKey').mockReturnValue(true);
- expect(addSshKeyValidation.submit()).toBeTruthy();
+ expect(addSshKeyValidation.submit()).toBe(true);
});
it('calls preventDefault and toggleWarning if isValid is false', () => {
- const addSshKeyValidation = new AddSshKeyValidation({});
+ const addSshKeyValidation = new AddSshKeyValidation([], {});
const event = {
preventDefault: jest.fn(),
};
- jest.spyOn(AddSshKeyValidation, 'isPublicKey').mockReturnValue(false);
+ jest.spyOn(addSshKeyValidation, 'isPublicKey').mockReturnValue(false);
jest.spyOn(addSshKeyValidation, 'toggleWarning').mockImplementation(() => {});
addSshKeyValidation.submit(event);
@@ -31,14 +31,15 @@ describe('AddSshKeyValidation', () => {
warningElement.classList.add('hide');
const addSshKeyValidation = new AddSshKeyValidation(
+ [],
{},
warningElement,
originalSubmitElement,
);
addSshKeyValidation.toggleWarning(true);
- expect(warningElement.classList.contains('hide')).toBeFalsy();
- expect(originalSubmitElement.classList.contains('hide')).toBeTruthy();
+ expect(warningElement.classList.contains('hide')).toBe(false);
+ expect(originalSubmitElement.classList.contains('hide')).toBe(true);
});
it('hides warningElement and shows originalSubmitElement if isVisible is false', () => {
@@ -47,25 +48,32 @@ describe('AddSshKeyValidation', () => {
originalSubmitElement.classList.add('hide');
const addSshKeyValidation = new AddSshKeyValidation(
+ [],
{},
warningElement,
originalSubmitElement,
);
addSshKeyValidation.toggleWarning(false);
- expect(warningElement.classList.contains('hide')).toBeTruthy();
- expect(originalSubmitElement.classList.contains('hide')).toBeFalsy();
+ expect(warningElement.classList.contains('hide')).toBe(true);
+ expect(originalSubmitElement.classList.contains('hide')).toBe(false);
});
});
describe('isPublicKey', () => {
- it('returns false if probably invalid public ssh key', () => {
- expect(AddSshKeyValidation.isPublicKey('nope')).toBeFalsy();
+ it('returns false if value begins with an algorithm name that is unsupported', () => {
+ const addSshKeyValidation = new AddSshKeyValidation(['ssh-rsa', 'ssh-algorithm'], {});
+
+ expect(addSshKeyValidation.isPublicKey('nope key')).toBe(false);
+ expect(addSshKeyValidation.isPublicKey('ssh- key')).toBe(false);
+ expect(addSshKeyValidation.isPublicKey('unsupported-ssh-rsa key')).toBe(false);
});
- it('returns true if probably valid public ssh key', () => {
- expect(AddSshKeyValidation.isPublicKey('ssh-')).toBeTruthy();
- expect(AddSshKeyValidation.isPublicKey('ecdsa-sha2-')).toBeTruthy();
+ it('returns true if value begins with an algorithm name that is supported', () => {
+ const addSshKeyValidation = new AddSshKeyValidation(['ssh-rsa', 'ssh-algorithm'], {});
+
+ expect(addSshKeyValidation.isPublicKey('ssh-rsa key')).toBe(true);
+ expect(addSshKeyValidation.isPublicKey('ssh-algorithm key')).toBe(true);
});
});
});
diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js
index 5cdc3d174a1..40e7d27edc8 100644
--- a/spec/frontend/project_select_combo_button_spec.js
+++ b/spec/frontend/project_select_combo_button_spec.js
@@ -28,7 +28,7 @@ describe('Project Select Combo Button', () => {
loadFixtures(fixturePath);
- testContext.newItemBtn = document.querySelector('.new-project-item-link');
+ testContext.newItemBtn = document.querySelector('.js-new-project-item-link');
testContext.projectSelectInput = document.querySelector('.project-item-select');
});
@@ -120,7 +120,6 @@ describe('Project Select Combo Button', () => {
const returnedVariants = testContext.method();
expect(returnedVariants.localStorageItemType).toBe('new-merge-request');
- expect(returnedVariants.defaultTextPrefix).toBe('New merge request');
expect(returnedVariants.presetTextSuffix).toBe('merge request');
});
@@ -131,7 +130,6 @@ describe('Project Select Combo Button', () => {
const returnedVariants = testContext.method();
expect(returnedVariants.localStorageItemType).toBe('new-issue');
- expect(returnedVariants.defaultTextPrefix).toBe('New issue');
expect(returnedVariants.presetTextSuffix).toBe('issue');
});
});
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 60d36597fda..23b4cccd92c 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -65,6 +65,8 @@ describe('Author Select', () => {
describe('user is searching via "filter by commit message"', () => {
it('disables dropdown container', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
return wrapper.vm.$nextTick().then(() => {
@@ -73,6 +75,8 @@ describe('Author Select', () => {
});
it('has correct tooltip message', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
return wrapper.vm.$nextTick().then(() => {
@@ -83,6 +87,8 @@ describe('Author Select', () => {
});
it('disables dropdown', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: false });
return wrapper.vm.$nextTick().then(() => {
@@ -103,6 +109,8 @@ describe('Author Select', () => {
});
it('displays the current selected author', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
return wrapper.vm.$nextTick().then(() => {
@@ -156,6 +164,8 @@ describe('Author Select', () => {
isChecked: true,
};
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index 38e13dc5462..eb80d57fb3c 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -101,6 +101,8 @@ describe('RevisionDropdown component', () => {
const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ branches: ['some-branch'] });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index 106b41bcc02..9c1000039b1 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -3,7 +3,7 @@ import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
-import ProjectFindFile from '~/project_find_file';
+import ProjectFindFile from '~/projects/project_find_file';
jest.mock('~/lib/dompurify', () => ({
addHook: jest.fn(),
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 9f9d574a8ed..d5b882bd715 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -1,6 +1,5 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@@ -16,6 +15,7 @@ const DEFAULT_PROPS = {
projectPath: 'some/project/path',
isLocked: false,
canLock: true,
+ showForkSuggestion: false,
};
const DEFAULT_INJECT = {
@@ -27,7 +27,7 @@ describe('BlobButtonGroup component', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = shallowMount(BlobButtonGroup, {
+ wrapper = mountExtended(BlobButtonGroup, {
propsData: {
...DEFAULT_PROPS,
...props,
@@ -35,9 +35,6 @@ describe('BlobButtonGroup component', () => {
provide: {
...DEFAULT_INJECT,
},
- directives: {
- GlModal: createMockDirective(),
- },
});
};
@@ -47,7 +44,8 @@ describe('BlobButtonGroup component', () => {
const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
- const findReplaceButton = () => wrapper.find('[data-testid="replace"]');
+ const findDeleteButton = () => wrapper.findByTestId('delete');
+ const findReplaceButton = () => wrapper.findByTestId('replace');
it('renders component', () => {
createComponent();
@@ -63,6 +61,8 @@ describe('BlobButtonGroup component', () => {
describe('buttons', () => {
beforeEach(() => {
createComponent();
+ jest.spyOn(findUploadBlobModal().vm, 'show');
+ jest.spyOn(findDeleteBlobModal().vm, 'show');
});
it('renders both the replace and delete button', () => {
@@ -75,10 +75,37 @@ describe('BlobButtonGroup component', () => {
});
it('triggers the UploadBlobModal from the replace button', () => {
- const { value } = getBinding(findReplaceButton().element, 'gl-modal');
- const modalId = findUploadBlobModal().props('modalId');
+ findReplaceButton().trigger('click');
+
+ expect(findUploadBlobModal().vm.show).toHaveBeenCalled();
+ });
+
+ it('triggers the DeleteBlobModal from the delete button', () => {
+ findDeleteButton().trigger('click');
+
+ expect(findDeleteBlobModal().vm.show).toHaveBeenCalled();
+ });
+
+ describe('showForkSuggestion set to true', () => {
+ beforeEach(() => {
+ createComponent({ showForkSuggestion: true });
+ jest.spyOn(findUploadBlobModal().vm, 'show');
+ jest.spyOn(findDeleteBlobModal().vm, 'show');
+ });
+
+ it('does not trigger the UploadBlobModal from the replace button', () => {
+ findReplaceButton().trigger('click');
+
+ expect(findUploadBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(wrapper.emitted().fork).toBeTruthy();
+ });
+
+ it('does not trigger the DeleteBlobModal from the delete button', () => {
+ findDeleteButton().trigger('click');
- expect(modalId).toEqual(value);
+ expect(findDeleteBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(wrapper.emitted().fork).toBeTruthy();
+ });
});
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 9e00a2d0408..d3b60ec3768 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -83,6 +83,8 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
}),
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ project, isBinary });
await waitForPromises();
@@ -336,35 +338,11 @@ describe('Blob content viewer component', () => {
deletePath: webPath,
canPushCode: pushCode,
canLock: true,
- isLocked: true,
+ isLocked: false,
emptyRepo: empty,
});
});
- it.each`
- canPushCode | canDownloadCode | username | canLock
- ${true} | ${true} | ${'root'} | ${true}
- ${false} | ${true} | ${'root'} | ${false}
- ${true} | ${false} | ${'root'} | ${false}
- ${true} | ${true} | ${'peter'} | ${false}
- `(
- 'passes the correct lock states',
- async ({ canPushCode, canDownloadCode, username, canLock }) => {
- gon.current_username = username;
-
- await createComponent(
- {
- pushCode: canPushCode,
- downloadCode: canDownloadCode,
- empty,
- },
- mount,
- );
-
- expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
- },
- );
-
it('does not render if not logged in', async () => {
isLoggedIn.mockReturnValueOnce(false);
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
new file mode 100644
index 00000000000..03e389ea5cb
--- /dev/null
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -0,0 +1,88 @@
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import BlobControls from '~/repository/components/blob_controls.vue';
+import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createRouter from '~/repository/router';
+import { updateElementsVisibility } from '~/repository/utils/dom';
+import { blobControlsDataMock, refMock } from '../mock_data';
+
+jest.mock('~/repository/utils/dom');
+
+let router;
+let wrapper;
+let mockResolver;
+
+const localVue = createLocalVue();
+
+const createComponent = async () => {
+ localVue.use(VueApollo);
+
+ const project = { ...blobControlsDataMock };
+ const projectPath = 'some/project';
+
+ router = createRouter(projectPath, refMock);
+
+ router.replace({ name: 'blobPath', params: { path: '/some/file.js' } });
+
+ mockResolver = jest.fn().mockResolvedValue({ data: { project } });
+
+ wrapper = shallowMountExtended(BlobControls, {
+ localVue,
+ router,
+ apolloProvider: createMockApollo([[blobControlsQuery, mockResolver]]),
+ propsData: { projectPath },
+ mixins: [{ data: () => ({ ref: refMock }) }],
+ });
+
+ await waitForPromises();
+};
+
+describe('Blob controls component', () => {
+ const findFindButton = () => wrapper.findByTestId('find');
+ const findBlameButton = () => wrapper.findByTestId('blame');
+ const findHistoryButton = () => wrapper.findByTestId('history');
+ const findPermalinkButton = () => wrapper.findByTestId('permalink');
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ it('renders a find button with the correct href', () => {
+ expect(findFindButton().attributes('href')).toBe('find/file.js');
+ });
+
+ it('renders a blame button with the correct href', () => {
+ expect(findBlameButton().attributes('href')).toBe('blame/file.js');
+ });
+
+ it('renders a history button with the correct href', () => {
+ expect(findHistoryButton().attributes('href')).toBe('history/file.js');
+ });
+
+ it('renders a permalink button with the correct href', () => {
+ expect(findPermalinkButton().attributes('href')).toBe('permalink/file.js');
+ });
+
+ it.each`
+ name | path
+ ${'blobPathDecoded'} | ${null}
+ ${'treePathDecoded'} | ${'myFile.js'}
+ `(
+ 'does not render any buttons if router name is $name and router path is $path',
+ async ({ name, path }) => {
+ router.replace({ name, params: { path } });
+
+ await nextTick();
+
+ expect(findFindButton().exists()).toBe(false);
+ expect(findBlameButton().exists()).toBe(false);
+ expect(findHistoryButton().exists()).toBe(false);
+ expect(findPermalinkButton().exists()).toBe(false);
+ expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true);
+ },
+ );
+});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index eb957c635ac..ad2cbd70187 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -75,6 +75,8 @@ describe('Repository breadcrumbs component', () => {
it('does not render add to tree dropdown when permissions are false', async () => {
factory('/', { canCollaborate: false });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
await wrapper.vm.$nextTick();
@@ -100,6 +102,8 @@ describe('Repository breadcrumbs component', () => {
it('renders add to tree dropdown when permissions are true', async () => {
factory('/', { canCollaborate: true });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
await wrapper.vm.$nextTick();
@@ -117,6 +121,8 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
await wrapper.vm.$nextTick();
@@ -139,6 +145,8 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index ebea7dde34a..fe05a981845 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -43,6 +43,8 @@ function factory(commit = createCommitData(), loading = false) {
},
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ commit });
vm.vm.$apollo.queries.commit.loading = loading;
}
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index 466eed52739..2490258a048 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -34,6 +34,8 @@ describe('Repository file preview component', () => {
name: 'README.md',
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ readme: { html: '<div class="blob">test</div>' } });
return vm.vm.$nextTick(() => {
@@ -47,6 +49,8 @@ describe('Repository file preview component', () => {
name: 'README.md',
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ readme: { html: '<div class="blob">test</div>' } });
return vm.vm
@@ -63,6 +67,8 @@ describe('Repository file preview component', () => {
name: 'README.md',
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ loading: 1 });
return vm.vm.$nextTick(() => {
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index c8dddefc4f2..2cd88944f81 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -89,6 +89,8 @@ describe('Repository table component', () => {
`('renders table caption for $ref in $path', ({ path, ref }) => {
factory({ path });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ ref });
return vm.vm.$nextTick(() => {
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 7f59dbfe0d1..440baa72a3c 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -40,6 +40,8 @@ function factory(propsData = {}) {
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ escapedRef: 'main' });
}
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 9c5d07eede3..00ad1fc05f6 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -46,6 +46,8 @@ describe('Repository table component', () => {
it('renders file preview', async () => {
factory('/');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
await vm.vm.$nextTick();
@@ -134,6 +136,8 @@ describe('Repository table component', () => {
it('is not rendered if less than 1000 files', async () => {
factory('/');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ fetchCounter: 5, clickedShowMore: false });
await vm.vm.$nextTick();
@@ -153,6 +157,8 @@ describe('Repository table component', () => {
factory('/');
const blobs = new Array(totalBlobs).fill('fakeBlob');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ entries: { blobs }, pagesLoaded });
await vm.vm.$nextTick();
@@ -173,6 +179,8 @@ describe('Repository table component', () => {
${200} | ${100}
`('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
factory('/');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ fetchCounter });
vm.vm.fetchFiles();
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index e9dfa3cd495..6b8b0752485 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -109,6 +109,8 @@ describe('UploadBlobModal', () => {
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ target: 'Not main' });
await wrapper.vm.$nextTick();
@@ -120,6 +122,8 @@ describe('UploadBlobModal', () => {
describe('completed form', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
file: { type: 'jpg' },
filePreviewURL: 'http://file.com?format=jpg',
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 74d35daf578..a5ee17ba672 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -13,7 +13,9 @@ export const simpleViewerMock = {
ideForkAndEditPath: 'some_file.js/fork/ide',
canModifyBlob: true,
canCurrentUserPushToBranch: true,
+ archived: false,
storedExternally: false,
+ externalStorage: 'lfs',
rawPath: 'some_file.js',
replacePath: 'some_file.js/replace',
pipelineEditorPath: '',
@@ -50,7 +52,7 @@ export const projectMock = {
nodes: [
{
id: 'test',
- path: simpleViewerMock.path,
+ path: 'locked_file.js',
user: { id: '123', username: 'root' },
},
],
@@ -63,3 +65,22 @@ export const projectMock = {
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';
+
+export const blobControlsDataMock = {
+ id: '1234',
+ repository: {
+ blobs: {
+ nodes: [
+ {
+ id: '5678',
+ findFilePath: 'find/file.js',
+ blamePath: 'blame/file.js',
+ historyPath: 'history/file.js',
+ permalinkPath: 'permalink/file.js',
+ storedExternally: false,
+ externalStorage: '',
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
index 1a1428e8cb1..ad0bce5c9af 100644
--- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js
+++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
@@ -2,12 +2,12 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import RunnerHeader from '~/runner/components/runner_header.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
-import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue';
+import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue';
import { captureException } from '~/runner/sentry_utils';
import { runnerData } from '../mock_data';
@@ -21,14 +21,14 @@ const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
const localVue = createLocalVue();
localVue.use(VueApollo);
-describe('RunnerDetailsApp', () => {
+describe('AdminRunnerEditApp', () => {
let wrapper;
let mockRunnerQuery;
- const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
+ const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
- wrapper = mountFn(RunnerDetailsApp, {
+ wrapper = mountFn(AdminRunnerEditApp, {
localVue,
apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]),
propsData: {
@@ -40,7 +40,7 @@ describe('RunnerDetailsApp', () => {
return waitForPromises();
};
- beforeEach(async () => {
+ beforeEach(() => {
mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
});
@@ -56,15 +56,16 @@ describe('RunnerDetailsApp', () => {
});
it('displays the runner id', async () => {
- await createComponentWithApollo();
+ await createComponentWithApollo({ mountFn: mount });
- expect(wrapper.text()).toContain(`Runner #${mockRunnerId}`);
+ expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`);
});
- it('displays the runner type', async () => {
+ it('displays the runner type and status', async () => {
await createComponentWithApollo({ mountFn: mount });
- expect(findRunnerTypeBadge().text()).toBe('shared');
+ expect(findRunnerHeader().text()).toContain(`never contacted`);
+ expect(findRunnerHeader().text()).toContain(`shared`);
});
describe('When there is an error', () => {
@@ -73,15 +74,15 @@ describe('RunnerDetailsApp', () => {
await createComponentWithApollo();
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Network error: Error!'),
- component: 'RunnerDetailsApp',
+ component: 'AdminRunnerEditApp',
});
});
- it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalled();
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 7015fe809b0..42be691ba4c 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -22,23 +23,21 @@ import {
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
+import getRunnersCountQuery from '~/runner/graphql/get_runners_count.query.graphql';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { runnersData, runnersDataPaginated } from '../mock_data';
+import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockActiveRunnersCount = '2';
-const mockAllRunnersCount = '6';
-const mockInstanceRunnersCount = '3';
-const mockGroupRunnersCount = '2';
-const mockProjectRunnersCount = '1';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -53,7 +52,9 @@ localVue.use(VueApollo);
describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
+ let mockRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
@@ -65,27 +66,28 @@ describe('AdminRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getRunnersQuery, mockRunnersQuery]];
-
- wrapper = mountFn(AdminRunnersApp, {
- localVue,
- apolloProvider: createMockApollo(handlers),
- propsData: {
- registrationToken: mockRegistrationToken,
- activeRunnersCount: mockActiveRunnersCount,
- allRunnersCount: mockAllRunnersCount,
- instanceRunnersCount: mockInstanceRunnersCount,
- groupRunnersCount: mockGroupRunnersCount,
- projectRunnersCount: mockProjectRunnersCount,
- ...props,
- },
- });
+ const handlers = [
+ [getRunnersQuery, mockRunnersQuery],
+ [getRunnersCountQuery, mockRunnersCountQuery],
+ ];
+
+ wrapper = extendedWrapper(
+ mountFn(AdminRunnersApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ }),
+ );
};
beforeEach(async () => {
setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
+ mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData);
createComponent();
await waitForPromises();
});
@@ -95,13 +97,71 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows the runner tabs with a runner count', async () => {
+ it('shows total runner counts', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 4');
+ expect(stats).toMatch('Offline runners 4');
+ expect(stats).toMatch('Stale runners 4');
+ });
+
+ it('shows the runner tabs with a runner count for each type', async () => {
+ mockRunnersCountQuery.mockImplementation(({ type }) => {
+ let count;
+ switch (type) {
+ case INSTANCE_TYPE:
+ count = 3;
+ break;
+ case GROUP_TYPE:
+ count = 2;
+ break;
+ case PROJECT_TYPE:
+ count = 1;
+ break;
+ default:
+ count = 6;
+ break;
+ }
+ return Promise.resolve({ data: { runners: { count } } });
+ });
+
+ createComponent({ mountFn: mount });
+ await waitForPromises();
+
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
- `All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`,
+ `All 6 Instance 3 Group 2 Project 1`,
+ );
+ });
+
+ it('shows the runner tabs with a formatted runner count', async () => {
+ mockRunnersCountQuery.mockImplementation(({ type }) => {
+ let count;
+ switch (type) {
+ case INSTANCE_TYPE:
+ count = 3000;
+ break;
+ case GROUP_TYPE:
+ count = 2000;
+ break;
+ case PROJECT_TYPE:
+ count = 1000;
+ break;
+ default:
+ count = 6000;
+ break;
+ }
+ return Promise.resolve({ data: { runners: { count } } });
+ });
+
+ createComponent({ mountFn: mount });
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ `All 6,000 Instance 3,000 Group 2,000 Project 1,000`,
);
});
@@ -152,12 +212,6 @@ describe('AdminRunnersApp', () => {
]);
});
- it('shows the active runner count', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`));
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
@@ -241,7 +295,7 @@ describe('AdminRunnersApp', () => {
});
it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 95c212cb0a9..4233d86c24c 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -4,7 +4,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { captureException } from '~/runner/sentry_utils';
@@ -40,15 +40,17 @@ describe('RunnerTypeCell', () => {
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value;
- const createComponent = ({ active = true } = {}, options) => {
+ const createComponent = (runner = {}, options) => {
wrapper = extendedWrapper(
shallowMount(RunnerActionCell, {
propsData: {
runner: {
id: mockRunner.id,
shortSha: mockRunner.shortSha,
- adminUrl: mockRunner.adminUrl,
- active,
+ editAdminUrl: mockRunner.editAdminUrl,
+ userPermissions: mockRunner.userPermissions,
+ active: mockRunner.active,
+ ...runner,
},
},
localVue,
@@ -101,7 +103,26 @@ describe('RunnerTypeCell', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
- expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl);
+ expect(findEditBtn().attributes('href')).toBe(mockRunner.editAdminUrl);
+ });
+
+ it('Does not render the runner edit link when user cannot update', () => {
+ createComponent({
+ userPermissions: {
+ ...mockRunner.userPermissions,
+ updateRunner: false,
+ },
+ });
+
+ expect(findEditBtn().exists()).toBe(false);
+ });
+
+ it('Does not render the runner edit link when editAdminUrl is not provided', () => {
+ createComponent({
+ editAdminUrl: null,
+ });
+
+ expect(findEditBtn().exists()).toBe(false);
});
});
@@ -179,7 +200,7 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
@@ -208,11 +229,22 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
});
});
+
+ it('Does not render the runner toggle active button when user cannot update', () => {
+ createComponent({
+ userPermissions: {
+ ...mockRunner.userPermissions,
+ updateRunner: false,
+ },
+ });
+
+ expect(findToggleActiveBtn().exists()).toBe(false);
+ });
});
describe('Delete action', () => {
@@ -225,6 +257,10 @@ describe('RunnerTypeCell', () => {
);
});
+ it('Renders delete button', () => {
+ expect(findDeleteBtn().exists()).toBe(true);
+ });
+
it('Delete button opens delete modal', () => {
const modalId = getBinding(findDeleteBtn().element, 'gl-modal').value;
@@ -259,6 +295,18 @@ describe('RunnerTypeCell', () => {
});
});
+ it('Does not render the runner delete button when user cannot delete', () => {
+ createComponent({
+ userPermissions: {
+ ...mockRunner.userPermissions,
+ deleteRunner: false,
+ },
+ });
+
+ expect(findDeleteBtn().exists()).toBe(false);
+ expect(findRunnerDeleteModal().exists()).toBe(false);
+ });
+
describe('When delete is clicked', () => {
beforeEach(() => {
findRunnerDeleteModal().vm.$emit('primary');
@@ -302,7 +350,7 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('toast notification is not shown', () => {
@@ -334,7 +382,7 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 0d002c272b4..e75decddf70 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,14 +1,15 @@
-import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -18,14 +19,18 @@ localVue.use(VueApollo);
localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
+const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
let showToast;
+ const mockEvent = { preventDefault: jest.fn() };
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
const createComponent = ({ props, provide = {} } = {}) => {
wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
@@ -38,6 +43,9 @@ describe('RegistrationTokenResetDropdownItem', () => {
apolloProvider: createMockApollo([
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
+ directives: {
+ GlModal: createMockDirective(),
+ },
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
@@ -54,8 +62,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
});
createComponent();
-
- jest.spyOn(window, 'confirm');
});
afterEach(() => {
@@ -66,6 +72,18 @@ describe('RegistrationTokenResetDropdownItem', () => {
expect(findDropdownItem().exists()).toBe(true);
});
+ describe('modal directive integration', () => {
+ it('has the correct ID on the dropdown', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
+ });
+
describe('On click and confirmation', () => {
const mockGroupId = '11';
const mockProjectId = '22';
@@ -82,9 +100,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
props: { type },
});
- window.confirm.mockReturnValueOnce(true);
-
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
});
@@ -114,7 +131,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
- window.confirm.mockReturnValueOnce(false);
findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -142,11 +158,11 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: `Network error: ${mockErrorMsg}`,
});
expect(captureException).toHaveBeenCalledWith({
@@ -168,11 +184,11 @@ describe('RegistrationTokenResetDropdownItem', () => {
},
});
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: `${mockErrorMsg} ${mockErrorMsg2}`,
});
expect(captureException).toHaveBeenCalledWith({
@@ -184,8 +200,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
- window.confirm.mockReturnValue(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js
new file mode 100644
index 00000000000..50699df3a44
--- /dev/null
+++ b/spec/frontend/runner/components/runner_header_spec.js
@@ -0,0 +1,93 @@
+import { GlSprintf } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
+import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import RunnerHeader from '~/runner/components/runner_header.vue';
+import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
+
+import { runnerData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+
+describe('RunnerHeader', () => {
+ let wrapper;
+
+ const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
+ const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
+ const findTimeAgo = () => wrapper.findComponent(TimeAgo);
+
+ const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerHeader, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ },
+ stubs: {
+ GlSprintf,
+ TimeAgo,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays the runner status', () => {
+ createComponent({
+ mountFn: mount,
+ runner: {
+ status: STATUS_ONLINE,
+ },
+ });
+
+ expect(findRunnerStatusBadge().text()).toContain(`online`);
+ });
+
+ it('displays the runner type', () => {
+ createComponent({
+ mountFn: mount,
+ runner: {
+ runnerType: GROUP_TYPE,
+ },
+ });
+
+ expect(findRunnerTypeBadge().text()).toContain(`group`);
+ });
+
+ it('displays the runner id', () => {
+ createComponent({
+ runner: {
+ id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Runner #99`);
+ });
+
+ it('displays the runner creation time', () => {
+ createComponent();
+
+ expect(wrapper.text()).toMatch(/created .+/);
+ expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('does not display runner creation time if createdAt missing', () => {
+ createComponent({
+ runner: {
+ id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ createdAt: null,
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Runner #99`);
+ expect(wrapper.text()).not.toMatch(/created .+/);
+ expect(findTimeAgo().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 5a14fa5a2d5..452430b7237 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -69,7 +69,9 @@ describe('RunnerList', () => {
const { id, description, version, ipAddress, shortSha } = mockRunners[0];
// Badges
- expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused');
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText(
+ 'never contacted paused',
+ );
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js
index a19515d6ed2..c470c6bb989 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/runner/components/runner_status_badge_spec.js
@@ -6,7 +6,6 @@ import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
- STATUS_NOT_CONNECTED,
STATUS_NEVER_CONTACTED,
} from '~/runner/constants';
@@ -50,20 +49,7 @@ describe('RunnerTypeBadge', () => {
expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
});
- it('renders not connected state', () => {
- createComponent({
- runner: {
- contactedAt: null,
- status: STATUS_NOT_CONNECTED,
- },
- });
-
- expect(wrapper.text()).toBe('not connected');
- expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toMatch('This runner has never connected');
- });
-
- it('renders never contacted state as not connected, for backwards compatibility', () => {
+ it('renders never contacted state', () => {
createComponent({
runner: {
contactedAt: null,
@@ -71,9 +57,9 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('not connected');
+ expect(wrapper.text()).toBe('never contacted');
expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toMatch('This runner has never connected');
+ expect(getTooltip().value).toMatch('This runner has never contacted');
});
it('renders offline state', () => {
diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js
deleted file mode 100644
index 4023c75c9a8..00000000000
--- a/spec/frontend/runner/components/runner_type_alert_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerTypeAlert from '~/runner/components/runner_type_alert.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-
-describe('RunnerTypeAlert', () => {
- let wrapper;
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(RunnerTypeAlert, {
- propsData: {
- type: INSTANCE_TYPE,
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- type | exampleText | anchor
- ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
- ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
- ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
- `('When it is an $type level runner', ({ type, exampleText, anchor }) => {
- beforeEach(() => {
- createComponent({ props: { type } });
- });
-
- it('Describes runner type', () => {
- expect(wrapper.text()).toMatch(exampleText);
- });
-
- it(`Shows an "info" variant`, () => {
- expect(findAlert().props('variant')).toBe('info');
- });
-
- it(`Links to anchor "${anchor}"`, () => {
- expect(findLink().attributes('href')).toBe(`/help/ci/runners/runners_scope${anchor}`);
- });
- });
-
- describe('When runner type is not correct', () => {
- it('Does not render content when type is missing', () => {
- createComponent({ props: { type: undefined } });
-
- expect(wrapper.html()).toBe('');
- });
-
- it('Validation fails for an incorrect type', () => {
- expect(() => {
- createComponent({ props: { type: 'NOT_A_TYPE' } });
- }).toThrow();
- });
- });
-});
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 0e0844a785b..ebb2e67d1e2 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
@@ -79,9 +79,9 @@ describe('RunnerUpdateForm', () => {
input: expect.objectContaining(submittedRunner),
});
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: expect.stringContaining('saved'),
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
expect(findSubmitDisabledAttr()).toBeUndefined();
@@ -127,7 +127,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
// Some fields are not submitted
- const { ipAddress, runnerType, ...submitted } = mockRunner;
+ const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
@@ -238,7 +238,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: `Network error: ${mockErrorMsg}`,
});
expect(captureException).toHaveBeenCalledWith({
@@ -262,7 +262,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: mockErrorMsg,
});
expect(captureException).not.toHaveBeenCalled();
diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
index 89c06ba2df4..52557ff716d 100644
--- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue';
@@ -168,8 +168,8 @@ describe('TagToken', () => {
});
it('error is shown', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: expect.any(String) });
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) });
});
});
diff --git a/spec/frontend/runner/components/stat/runner_online_stat_spec.js b/spec/frontend/runner/components/stat/runner_online_stat_spec.js
deleted file mode 100644
index 18f865aa22c..00000000000
--- a/spec/frontend/runner/components/stat/runner_online_stat_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue';
-
-describe('RunnerOnlineBadge', () => {
- let wrapper;
-
- const findSingleStat = () => wrapper.findComponent(GlSingleStat);
-
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerOnlineBadge, {
- propsData: {
- value: '99',
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Uses a success appearance', () => {
- createComponent({}, shallowMount);
-
- expect(findSingleStat().props('variant')).toBe('success');
- });
-
- it('Renders a value', () => {
- createComponent({}, mount);
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`));
- });
-});
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js
new file mode 100644
index 00000000000..68db8621ef0
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_stats_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStats', () => {
+ let wrapper;
+
+ const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerStats, {
+ propsData: {
+ onlineRunnersCount: 3,
+ offlineRunnersCount: 2,
+ staleRunnersCount: 1,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays all the stats', () => {
+ createComponent({ mountFn: mount });
+
+ const stats = wrapper.text();
+
+ expect(stats).toMatch('Online runners 3');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 1');
+ });
+
+ it.each`
+ i | status
+ ${0} | ${STATUS_ONLINE}
+ ${1} | ${STATUS_OFFLINE}
+ ${2} | ${STATUS_STALE}
+ `('Displays status types at index $i', ({ i, status }) => {
+ createComponent();
+
+ expect(findRunnerStatusStatAt(i).props('status')).toBe(status);
+ });
+});
diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
new file mode 100644
index 00000000000..3218272eac7
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
@@ -0,0 +1,67 @@
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStatusStat', () => {
+ let wrapper;
+
+ const findSingleStat = () => wrapper.findComponent(GlSingleStat);
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(RunnerStatusStat, {
+ propsData: {
+ status: STATUS_ONLINE,
+ value: 99,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ status | variant | title | badge
+ ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
+ ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
+ ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
+ `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => {
+ beforeEach(() => {
+ createComponent({ props: { status } }, mount);
+ });
+
+ it('Renders text', () => {
+ expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`));
+ });
+
+ it(`Uses variant ${variant}`, () => {
+ expect(findSingleStat().props('variant')).toBe(variant);
+ });
+ });
+
+ it('Formats stat number', () => {
+ createComponent({ props: { value: 1000 } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners 1,000');
+ });
+
+ it('Shows a null result', () => {
+ createComponent({ props: { value: null } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows an undefined result', () => {
+ createComponent({ props: { value: undefined } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows result for an unknown status', () => {
+ createComponent({ props: { status: 'UNKNOWN' } }, mount);
+
+ expect(wrapper.text()).toMatch('Runners 99');
+ });
+});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 4451100de19..034b7848f35 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -6,12 +6,13 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -26,10 +27,11 @@ import {
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
+import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data';
+import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
+ let mockGroupRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
+ const handlers = [
+ [getGroupRunnersQuery, mockGroupRunnersQuery],
+ [getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
+ ];
wrapper = mountFn(GroupRunnersApp, {
localVue,
@@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => {
setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
+ mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData);
createComponent();
await waitForPromises();
});
+ it('shows total runner counts', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 2');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 2');
+ });
+
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
@@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => {
);
});
- describe('shows the active runner count', () => {
- const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`);
-
- it('with a regular value', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount));
- });
-
- it('at the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000'));
- });
-
- it('over the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+'));
- });
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
@@ -236,7 +234,7 @@ describe('GroupRunnersApp', () => {
});
it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index b8d0f1273c7..9c430e205ea 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -2,17 +2,21 @@
// Admin queries
import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json';
+import runnersCountData from 'test_fixtures/graphql/runner/get_runners_count.query.graphql.json';
import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json';
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
// Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
+import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json';
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export {
runnerData,
+ runnersCountData,
runnersDataPaginated,
runnersData,
groupRunnersData,
+ groupRunnersCountData,
groupRunnersDataPaginated,
};
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index 0fc7917663e..aff1ec882bb 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -1,6 +1,7 @@
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
import {
searchValidator,
+ updateOutdatedUrl,
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
@@ -190,6 +191,23 @@ describe('search_params.js', () => {
});
});
+ describe('updateOutdatedUrl', () => {
+ it('returns null for urls that do not need updating', () => {
+ expect(updateOutdatedUrl('http://test.host/')).toBe(null);
+ expect(updateOutdatedUrl('http://test.host/?a=b')).toBe(null);
+ });
+
+ it('returns updated url for updating NOT_CONNECTED to NEVER_CONTACTED', () => {
+ expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED')).toBe(
+ 'http://test.host/admin/runners?status[]=NEVER_CONTACTED',
+ );
+
+ expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED&a=b')).toBe(
+ 'http://test.host/admin/runners?status[]=NEVER_CONTACTED&a=b',
+ );
+ });
+ });
+
describe('fromUrlQueryToSearch', () => {
examples.forEach(({ name, urlQuery, search }) => {
it(`Converts ${name} to a search object`, () => {
diff --git a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js b/spec/frontend/runner/runner_update_form_utils_spec.js
index 510b4e604ac..a633aee92f7 100644
--- a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js
+++ b/spec/frontend/runner/runner_update_form_utils_spec.js
@@ -1,8 +1,5 @@
import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
-import {
- modelToUpdateMutationVariables,
- runnerToModel,
-} from '~/runner/runner_details/runner_update_form_utils';
+import { modelToUpdateMutationVariables, runnerToModel } from '~/runner/runner_update_form_utils';
const mockId = 'gid://gitlab/Ci::Runner/1';
const mockDescription = 'Runner Desc.';
@@ -23,7 +20,7 @@ const mockModel = {
tagList: 'tag-1, tag-2',
};
-describe('~/runner/runner_details/runner_update_form_utils', () => {
+describe('~/runner/runner_update_form_utils', () => {
describe('runnerToModel', () => {
it('collects all model data', () => {
expect(runnerToModel(mockRunner)).toEqual(mockModel);
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index b21cf5c6b79..de1cefa9e9d 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -133,6 +133,8 @@ describe('Global Search Searchable Dropdown', () => {
describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => {
beforeEach(() => {
createComponent({}, { frequentItems });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchText });
});
@@ -202,6 +204,8 @@ describe('Global Search Searchable Dropdown', () => {
describe('not for the first time', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasBeenOpened: true });
findGlDropdown().vm.$emit('show');
});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 0a2b18caf25..cbdf7f53913 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlTab, GlTabs } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
@@ -77,6 +77,7 @@ describe('App component', () => {
const findMainHeading = () => wrapper.find('h1');
const findTab = () => wrapper.findComponent(GlTab);
const findTabs = () => wrapper.findAllComponents(GlTab);
+ const findGlTabs = () => wrapper.findComponent(GlTabs);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList);
@@ -154,6 +155,14 @@ describe('App component', () => {
expect(findTab().exists()).toBe(true);
});
+ it('passes the `sync-active-tab-with-query-params` prop', () => {
+ expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
+ });
+
+ it('lazy loads each tab', () => {
+ expect(findGlTabs().attributes('lazy')).not.toBe(undefined);
+ });
+
it('renders correct amount of tabs', () => {
expect(findTabs()).toHaveLength(expectedTabs.length);
});
@@ -161,6 +170,10 @@ describe('App component', () => {
it.each(expectedTabs)('renders the %s tab', (tabName) => {
expect(findByTestId(`${tabName}-tab`).exists()).toBe(true);
});
+
+ it.each(expectedTabs)('has the %s query-param-value', (tabName) => {
+ expect(findByTestId(`${tabName}-tab`).props('queryParamValue')).toBe(tabName);
+ });
});
it('renders right amount of feature cards for given props with correct props', () => {
@@ -182,10 +195,6 @@ describe('App component', () => {
expect(findComplianceViewHistoryLink().exists()).toBe(false);
expect(findSecurityViewHistoryLink().exists()).toBe(false);
});
-
- it('renders TrainingProviderList component', () => {
- expect(findTrainingProviderList().exists()).toBe(true);
- });
});
describe('Manage via MR Error Alert', () => {
@@ -432,6 +441,25 @@ describe('App component', () => {
});
});
+ describe('Vulnerability management', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ });
+
+ it('renders TrainingProviderList component', () => {
+ expect(findTrainingProviderList().exists()).toBe(true);
+ });
+
+ it('renders security training description', () => {
+ const vulnerabilityManagementTab = wrapper.findByTestId('vulnerability-management-tab');
+
+ expect(vulnerabilityManagementTab.text()).toContain(i18n.securityTrainingDescription);
+ });
+ });
+
describe('when secureVulnerabilityTraining feature flag is disabled', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 60cc36a634c..578248e696f 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,87 +1,192 @@
-import { GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
+import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { securityTrainingProviders, mockResolvers } from '../mock_data';
+import {
+ securityTrainingProviders,
+ createMockResolvers,
+ testProjectPath,
+ textProviderIds,
+} from '../mock_data';
Vue.use(VueApollo);
describe('TrainingProviderList component', () => {
let wrapper;
- let mockApollo;
- let mockSecurityTrainingProvidersData;
+ let apolloProvider;
- const createComponent = () => {
- mockApollo = createMockApollo([], mockResolvers);
+ const createApolloProvider = ({ resolvers } = {}) => {
+ apolloProvider = createMockApollo([], createMockResolvers({ resolvers }));
+ };
+ const createComponent = () => {
wrapper = shallowMount(TrainingProviderList, {
- apolloProvider: mockApollo,
+ provide: {
+ projectPath: testProjectPath,
+ },
+ apolloProvider,
});
};
const waitForQueryToBeLoaded = () => waitForPromises();
+ const waitForMutationToBeLoaded = waitForQueryToBeLoaded;
const findCards = () => wrapper.findAllComponents(GlCard);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
+ const findFirstToggle = () => findToggles().at(0);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findErrorAlert = () => wrapper.findComponent(GlAlert);
- beforeEach(() => {
- mockSecurityTrainingProvidersData = jest.fn();
- mockSecurityTrainingProvidersData.mockResolvedValue(securityTrainingProviders);
-
- createComponent();
- });
+ const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
+ apolloProvider = null;
});
- describe('when loading', () => {
- it('shows the loader', () => {
- expect(findLoader().exists()).toBe(true);
+ describe('with a successful response', () => {
+ beforeEach(() => {
+ createApolloProvider();
+ createComponent();
});
- it('does not show the cards', () => {
- expect(findCards().exists()).toBe(false);
+ describe('when loading', () => {
+ it('shows the loader', () => {
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('does not show the cards', () => {
+ expect(findCards().exists()).toBe(false);
+ });
});
- });
- describe('basic structure', () => {
- beforeEach(async () => {
- await waitForQueryToBeLoaded();
+ describe('basic structure', () => {
+ beforeEach(async () => {
+ await waitForQueryToBeLoaded();
+ });
+
+ it('renders correct amount of cards', () => {
+ expect(findCards()).toHaveLength(securityTrainingProviders.length);
+ });
+
+ securityTrainingProviders.forEach(({ name, description, url, isEnabled }, index) => {
+ it(`shows the name for card ${index}`, () => {
+ expect(findCards().at(index).text()).toContain(name);
+ });
+
+ it(`shows the description for card ${index}`, () => {
+ expect(findCards().at(index).text()).toContain(description);
+ });
+
+ it(`shows the learn more link for card ${index}`, () => {
+ expect(findLinks().at(index).attributes()).toEqual({
+ target: '_blank',
+ href: url,
+ });
+ });
+
+ it(`shows the toggle with the correct value for card ${index}`, () => {
+ expect(findToggles().at(index).props('value')).toEqual(isEnabled);
+ });
+
+ it('does not show loader when query is populated', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
+ });
});
- it('renders correct amount of cards', () => {
- expect(findCards()).toHaveLength(securityTrainingProviders.length);
+ describe('storing training provider settings', () => {
+ beforeEach(async () => {
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+
+ await waitForMutationToBeLoaded();
+
+ toggleFirstProvider();
+ });
+
+ it.each`
+ loading | wait | desc
+ ${true} | ${false} | ${'enables loading of GlToggle when mutation is called'}
+ ${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'}
+ `('$desc', async ({ loading, wait }) => {
+ if (wait) {
+ await waitForMutationToBeLoaded();
+ }
+ expect(findFirstToggle().props('isLoading')).toBe(loading);
+ });
+
+ it('calls mutation when toggle is changed', () => {
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ mutation: configureSecurityTrainingProvidersMutation,
+ variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
+ }),
+ );
+ });
});
+ });
+
+ describe('with errors', () => {
+ const expectErrorAlertToExist = () => {
+ expect(findErrorAlert().props()).toMatchObject({
+ dismissible: false,
+ variant: 'danger',
+ });
+ };
+
+ describe('when fetching training providers', () => {
+ beforeEach(async () => {
+ createApolloProvider({
+ resolvers: {
+ Query: {
+ securityTrainingProviders: jest.fn().mockReturnValue(new Error()),
+ },
+ },
+ });
+ createComponent();
- securityTrainingProviders.forEach(({ name, description, url, isEnabled }, index) => {
- it(`shows the name for card ${index}`, () => {
- expect(findCards().at(index).text()).toContain(name);
+ await waitForQueryToBeLoaded();
});
- it(`shows the description for card ${index}`, () => {
- expect(findCards().at(index).text()).toContain(description);
+ it('shows an non-dismissible error alert', () => {
+ expectErrorAlertToExist();
});
- it(`shows the learn more link for card ${index}`, () => {
- expect(findLinks().at(index).attributes()).toEqual({
- target: '_blank',
- href: url,
+ it('shows an error description', () => {
+ expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.providerQueryErrorMessage);
+ });
+ });
+
+ describe('when storing training provider configurations', () => {
+ beforeEach(async () => {
+ createApolloProvider({
+ resolvers: {
+ Mutation: {
+ configureSecurityTrainingProviders: () => ({
+ errors: ['something went wrong!'],
+ securityTrainingProviders: [],
+ }),
+ },
+ },
});
+ createComponent();
+
+ await waitForQueryToBeLoaded();
+ toggleFirstProvider();
+ await waitForMutationToBeLoaded();
});
- it(`shows the toggle with the correct value for card ${index}`, () => {
- expect(findToggles().at(index).props('value')).toEqual(isEnabled);
+ it('shows an non-dismissible error alert', () => {
+ expectErrorAlertToExist();
});
- it('does not show loader when query is populated', () => {
- expect(findLoader().exists()).toBe(false);
+ it('shows an error description', () => {
+ expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage);
});
});
});
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index cdb859c3800..37ecce3886d 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -1,16 +1,20 @@
+export const testProjectPath = 'foo/bar';
+
+export const textProviderIds = [101, 102];
+
export const securityTrainingProviders = [
{
- id: 101,
- name: 'Kontra',
- description: 'Interactive developer security education.',
- url: 'https://application.security/',
+ id: textProviderIds[0],
+ name: 'Vendor Name 1',
+ description: 'Interactive developer security education',
+ url: 'https://www.example.org/security/training',
isEnabled: false,
},
{
- id: 102,
- name: 'SecureCodeWarrior',
+ id: textProviderIds[1],
+ name: 'Vendor Name 2',
description: 'Security training with guide and learning pathways.',
- url: 'https://www.securecodewarrior.com/',
+ url: 'https://www.vendornametwo.com/',
isEnabled: true,
},
];
@@ -21,10 +25,15 @@ export const securityTrainingProvidersResponse = {
},
};
-export const mockResolvers = {
+const defaultMockResolvers = {
Query: {
securityTrainingProviders() {
return securityTrainingProviders;
},
},
};
+
+export const createMockResolvers = ({ resolvers: customMockResolvers = {} } = {}) => ({
+ ...defaultMockResolvers,
+ ...customMockResolvers,
+});
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
index c25a8d4bb92..350055cb935 100644
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyStateComponent should render content 1`] = `
-"<section class=\\"row empty-state text-center\\">
- <div class=\\"col-12\\">
+"<section class=\\"gl-display-flex empty-state gl-text-center gl-flex-direction-column\\">
+ <div class=\\"gl-max-w-full\\">
<div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"\\" role=\\"img\\" class=\\"gl-max-w-full\\"></div>
</div>
- <div class=\\"col-12\\">
- <div class=\\"text-content gl-mx-auto gl-my-0 gl-p-5\\">
+ <div class=\\"gl-max-w-full gl-m-auto\\">
+ <div class=\\"gl-mx-auto gl-my-0 gl-p-5\\">
<h1 class=\\"gl-font-size-h-display gl-line-height-36 h4\\">
Getting started with serverless
</h1>
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index d7261784edc..0c6ed998747 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -110,14 +110,23 @@ describe('SetStatusModalWrapper', () => {
});
describe('improvedEmojiPicker is true', () => {
+ const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
+
beforeEach(async () => {
await initEmojiMock();
wrapper = createComponent({}, true);
return initModal();
});
+ it('renders emoji picker dropdown with custom positioning', () => {
+ expect(getEmojiPicker().props()).toMatchObject({
+ right: false,
+ boundary: 'viewport',
+ });
+ });
+
it('sets emojiTag when clicking in emoji picker', async () => {
- await wrapper.findComponent(EmojiPicker).vm.$emit('click', 'thumbsup');
+ await getEmojiPicker().vm.$emit('click', 'thumbsup');
expect(wrapper.vm.emojiTag).toContain('data-name="thumbsup"');
});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 13887f28d22..d0792fa7b73 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -48,12 +48,16 @@ describe('UncollapsedReviewerList component', () => {
});
it('renders re-request loading icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 1: 'loading' } });
expect(wrapper.find('[data-testid="re-request-button"]').props('loading')).toBe(true);
});
it('renders re-request success icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 1: 'success' } });
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
@@ -98,6 +102,8 @@ describe('UncollapsedReviewerList component', () => {
});
it('renders re-request loading icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 2: 'loading' } });
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(2);
@@ -107,6 +113,8 @@ describe('UncollapsedReviewerList component', () => {
});
it('renders re-request success icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 2: 'success' } });
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(1);
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
index 1210f7c9531..94cdbe7f2ef 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -85,6 +85,8 @@ describe('Participants', () => {
numberOfLessParticipants,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: false,
});
@@ -101,6 +103,8 @@ describe('Participants', () => {
numberOfLessParticipants: 2,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: true,
});
@@ -129,6 +133,8 @@ describe('Participants', () => {
numberOfLessParticipants: 2,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: false,
});
@@ -145,6 +151,8 @@ describe('Participants', () => {
numberOfLessParticipants: 2,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: true,
});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 40bc6fe6aa5..c193bb08543 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -90,6 +90,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
/>
<!---->
+
+ <!---->
</div>
</div>
</div>
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index b92c1907980..172089f9ee6 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -157,6 +157,8 @@ describe('Blob Embeddable', () => {
});
// mimic apollo's update
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
blobContent: wrapper.vm.onContentUpdate(apolloData),
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 2d5e0cfd615..daa9d6345b0 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -242,6 +242,8 @@ describe('Snippet header component', () => {
// TODO: we should avoid `wrapper.setData` since they
// are component internals. Let's use the apollo mock helpers
// in a follow-up.
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ canCreateSnippet: true });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index 1d6245e9dbb..a833fd9ff9e 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -132,6 +132,8 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
describe('when the mode changes', () => {
const setInitialMode = (mode) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ editorMode: mode });
};
@@ -207,6 +209,8 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
it('syncs matter changes to content in markdown mode', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ editorMode: EDITOR_TYPES.markdown });
const newSettings = { title: 'test' };
diff --git a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
index 86ae016987d..c8c9f45618d 100644
--- a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -48,6 +48,8 @@ describe('Add Image Modal', () => {
const file = { name: 'some_file.png' };
wrapper.vm.$refs.uploadImageTab = { validateFile: jest.fn() };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ file, description, tabIndex: IMAGE_TABS.UPLOAD_TAB });
findModal().vm.$emit('ok', { preventDefault });
@@ -60,6 +62,8 @@ describe('Add Image Modal', () => {
it('emits an addImage event when a valid URL is specified', () => {
const preventDefault = jest.fn();
const mockImage = { imageUrl: '/some/valid/url.png', description: 'some description' };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ ...mockImage, tabIndex: IMAGE_TABS.URL_TAB });
findModal().vm.$emit('ok', { preventDefault });
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
index 6a2b89a8dcf..ddc96ed6832 100644
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -13,7 +13,7 @@ const normalParagraphNode = buildMockParagraphNode(
'This is just normal paragraph. It has multiple sentences.',
);
const identifierParagraphNode = buildMockParagraphNode(
- `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`,
+ `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example.org`,
);
describe('rich_content_editor/renderers_render_identifier_paragraph', () => {
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 9d28e8ce294..fbe55306f37 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -293,6 +293,8 @@ describe('StatesTableActions', () => {
describe('when state name is present', () => {
beforeEach(async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ removeConfirmText: defaultProps.state.name });
findRemoveModal().vm.$emit('ok');
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index 2b70aacc4cb..f1628ad9793 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -81,7 +81,8 @@ describe('Tracking', () => {
it('should activate features based on what has been enabled', () => {
initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]);
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [standardContext]);
+ expect(snowplowSpy).toHaveBeenCalledWith('setDocumentTitle', 'GitLab');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
@@ -130,7 +131,7 @@ describe('Tracking', () => {
it('includes those contexts alongside the standard context', () => {
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [
standardContext,
...experimentContexts,
]);
diff --git a/spec/frontend/version_check_image_spec.js b/spec/frontend/version_check_image_spec.js
deleted file mode 100644
index 13bd104a91c..00000000000
--- a/spec/frontend/version_check_image_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import $ from 'jquery';
-import ClassSpecHelper from 'helpers/class_spec_helper';
-import VersionCheckImage from '~/version_check_image';
-
-describe('VersionCheckImage', () => {
- let testContext;
-
- beforeEach(() => {
- testContext = {};
- });
-
- describe('bindErrorEvent', () => {
- ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
-
- beforeEach(() => {
- testContext.imageElement = $('<div></div>');
- });
-
- it('registers an error event', () => {
- jest.spyOn($.prototype, 'on').mockImplementation(() => {});
- // eslint-disable-next-line func-names
- jest.spyOn($.prototype, 'off').mockImplementation(function () {
- return this;
- });
-
- VersionCheckImage.bindErrorEvent(testContext.imageElement);
-
- expect($.prototype.off).toHaveBeenCalledWith('error');
- expect($.prototype.on).toHaveBeenCalledWith('error', expect.any(Function));
- });
-
- it('hides the imageElement on error', () => {
- jest.spyOn($.prototype, 'hide').mockImplementation(() => {});
-
- VersionCheckImage.bindErrorEvent(testContext.imageElement);
-
- testContext.imageElement.trigger('error');
-
- expect($.prototype.hide).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index af6624a6c43..36850e623c7 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -101,6 +101,8 @@ describe('MRWidget approvals', () => {
});
it('shows loading message', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ fetchingApprovals: true });
return tick().then(() => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
index a09269e869c..5a1f17573d4 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
@@ -153,6 +153,8 @@ describe('MRWidgetHeader', () => {
gitpodEnabled: true,
showGitpodButton: true,
gitpodUrl: 'http://gitpod.localhost',
+ userPreferencesGitpodPath: '/-/profile/preferences#user_gitpod_enabled',
+ userProfileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
};
it('renders checkout branch button with modal trigger', () => {
@@ -208,6 +210,8 @@ describe('MRWidgetHeader', () => {
gitpodEnabled: true,
showGitpodButton: true,
gitpodUrl: 'http://gitpod.localhost',
+ userPreferencesGitpodPath: mrDefaultOptions.userPreferencesGitpodPath,
+ userProfileEnableGitpodPath: mrDefaultOptions.userProfileEnableGitpodPath,
webIdeUrl,
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
index d3221cc2fc7..27604868b3e 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -2,10 +2,15 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+import {
+ REBASE_BUTTON_KEY,
+ REBASE_WITHOUT_CI_BUTTON_KEY,
+} from '~/vue_merge_request_widget/constants';
let wrapper;
-function factory(propsData, mergeRequestWidgetGraphql) {
+function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) {
wrapper = shallowMount(WidgetRebase, {
propsData,
data() {
@@ -19,7 +24,7 @@ function factory(propsData, mergeRequestWidgetGraphql) {
},
};
},
- provide: { glFeatures: { mergeRequestWidgetGraphql } },
+ provide: { glFeatures: { mergeRequestWidgetGraphql, rebaseWithoutCiUi } },
mocks: {
$apollo: {
queries: {
@@ -31,8 +36,10 @@ function factory(propsData, mergeRequestWidgetGraphql) {
}
describe('Merge request widget rebase component', () => {
- const findRebaseMessageEl = () => wrapper.find('[data-testid="rebase-message"]');
- const findRebaseMessageElText = () => findRebaseMessageEl().text();
+ const findRebaseMessage = () => wrapper.find('[data-testid="rebase-message"]');
+ const findRebaseMessageText = () => findRebaseMessage().text();
+ const findRebaseButtonActions = () => wrapper.find(ActionsButton);
+ const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]');
afterEach(() => {
wrapper.destroy();
@@ -40,10 +47,10 @@ describe('Merge request widget rebase component', () => {
});
[true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
- describe('While rebasing', () => {
+ describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
+ describe('while rebasing', () => {
it('should show progress message', () => {
- factory(
+ createWrapper(
{
mr: { rebaseInProgress: true },
service: {},
@@ -51,24 +58,30 @@ describe('Merge request widget rebase component', () => {
mergeRequestWidgetGraphql,
);
- expect(findRebaseMessageElText()).toContain('Rebase in progress');
+ expect(findRebaseMessageText()).toContain('Rebase in progress');
});
});
- describe('With permissions', () => {
- it('it should render rebase button and warning message', () => {
- factory(
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
+
+ it('renders the warning message', () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: true,
},
- service: {},
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
mergeRequestWidgetGraphql,
);
- const text = findRebaseMessageElText();
+ const text = findRebaseMessageText();
expect(text).toContain('Merge blocked');
expect(text.replace(/\s\s+/g, ' ')).toContain(
@@ -76,73 +89,195 @@ describe('Merge request widget rebase component', () => {
);
});
- it('it should render error message when it fails', async () => {
- factory(
+ it('renders an error message when rebasing has failed', async () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: true,
},
- service: {},
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
mergeRequestWidgetGraphql,
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ rebasingError: 'Something went wrong!' });
await nextTick();
- expect(findRebaseMessageElText()).toContain('Something went wrong!');
+ expect(findRebaseMessageText()).toContain('Something went wrong!');
+ });
+
+ describe('Rebase button with flag rebaseWithoutCiUi', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ mergeRequestWidgetGraphql,
+ { rebaseWithoutCiUi: true },
+ );
+ });
+
+ it('rebase button with actions is rendered', () => {
+ expect(findRebaseButtonActions().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(false);
+ });
+
+ it('has rebase and rebase without CI actions', () => {
+ const actionNames = findRebaseButtonActions()
+ .props('actions')
+ .map((action) => action.key);
+
+ expect(actionNames).toStrictEqual([REBASE_BUTTON_KEY, REBASE_WITHOUT_CI_BUTTON_KEY]);
+ });
+
+ it('defaults to rebase action', () => {
+ expect(findRebaseButtonActions().props('selectedKey')).toStrictEqual(REBASE_BUTTON_KEY);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ // ActionButtons use the actions props instead of emitting
+ // a click event, therefore simulating the behavior here:
+ findRebaseButtonActions()
+ .props('actions')
+ .find((x) => x.key === REBASE_BUTTON_KEY)
+ .handle();
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ // ActionButtons use the actions props instead of emitting
+ // a click event, therefore simulating the behavior here:
+ findRebaseButtonActions()
+ .props('actions')
+ .find((x) => x.key === REBASE_WITHOUT_CI_BUTTON_KEY)
+ .handle();
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('Rebase button with rebaseWithoutCiUI flag disabled', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ mergeRequestWidgetGraphql,
+ );
+ });
+
+ it('standard rebase button is rendered', () => {
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ expect(findRebaseButtonActions().exists()).toBe(false);
+ });
+
+ it('calls rebase method with skip_ci false', () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
});
});
- describe('Without permissions', () => {
- it('should render a message explaining user does not have permissions', () => {
- factory(
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
+
+ describe('UI text', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ },
+ service: {},
+ },
+ mergeRequestWidgetGraphql,
+ );
+ });
+
+ it('renders a message explaining user does not have permissions', () => {
+ const text = findRebaseMessageText();
+
+ expect(text).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ expect(text).toContain('the source branch must be rebased');
+ });
+
+ it('renders the correct target branch name', () => {
+ const elem = findRebaseMessage();
+
+ expect(elem.text()).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ });
+ });
+
+ it('does not render the rebase actions button with rebaseWithoutCiUI flag enabled', () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: false,
- targetBranch: 'foo',
+ targetBranch: exampleTargetBranch,
},
service: {},
},
mergeRequestWidgetGraphql,
+ { rebaseWithoutCiUi: true },
);
- const text = findRebaseMessageElText();
-
- expect(text).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
- expect(text).toContain('the source branch must be rebased');
+ expect(findRebaseButtonActions().exists()).toBe(false);
});
- it('should render the correct target branch name', () => {
- const targetBranch = 'fake-branch-to-test-with';
- factory(
+ it('does not render the standard rebase button with rebaseWithoutCiUI flag disabled', () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: false,
- targetBranch,
+ targetBranch: exampleTargetBranch,
},
service: {},
},
mergeRequestWidgetGraphql,
);
- const elem = findRebaseMessageEl();
-
- expect(elem.text()).toContain(
- `Merge blocked: the source branch must be rebased onto the target branch.`,
- );
+ expect(findStandardRebaseButton().exists()).toBe(false);
});
});
describe('methods', () => {
it('checkRebaseStatus', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- factory(
+ createWrapper(
{
mr: {},
service: {
diff --git a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
index bdad0bada5f..1900b53ac11 100644
--- a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
@@ -15,35 +15,12 @@ describe('Merge request widget merge checks failed state component', () => {
});
it.each`
- mrState | displayText
- ${{ isPipelineFailed: true }} | ${'pipelineFailed'}
- ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
- ${{ hasMergeableDiscussionsState: true }} | ${'unresolvedDiscussions'}
+ mrState | displayText
+ ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
+ ${{ blockingMergeRequests: { total_count: 1 } }} | ${'blockingMergeRequests'}
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
expect(wrapper.text()).toContain(MergeChecksFailed.i18n[displayText]);
});
-
- describe('unresolved discussions', () => {
- it('renders jump to button', () => {
- factory({ mr: { hasMergeableDiscussionsState: true } });
-
- expect(wrapper.find('[data-testid="jumpToUnresolved"]').exists()).toBe(true);
- });
-
- it('renders resolve thread button', () => {
- factory({
- mr: {
- hasMergeableDiscussionsState: true,
- createIssueToResolveDiscussionsPath: 'https://gitlab.com',
- },
- });
-
- expect(wrapper.find('[data-testid="resolveIssue"]').exists()).toBe(true);
- expect(wrapper.find('[data-testid="resolveIssue"]').attributes('href')).toBe(
- 'https://gitlab.com',
- );
- });
- });
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index d0a6af9970e..52a56af454f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -253,6 +253,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
factory({
...defaultMrProps(),
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isCancellingAutoMerge: true,
});
@@ -287,6 +289,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
factory({
...defaultMrProps(),
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isRemovingSourceBranch: true,
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
index 5858654e518..4d05e732f48 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -60,6 +60,8 @@ describe('Commits header component', () => {
it('has a chevron-right icon', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ expanded: false });
return wrapper.vm.$nextTick().then(() => {
@@ -111,6 +113,8 @@ describe('Commits header component', () => {
describe('when expanded', () => {
beforeEach(() => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ expanded: true });
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 89de160b02f..ec222e66a97 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -41,6 +41,8 @@ describe('MRWidgetConflicts', () => {
);
if (mergeRequestWidgetGraphql) {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
userPermissions: {
canMerge: propsData.mr.canMerge,
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
index 848677bf4d2..936d673768c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -14,6 +14,8 @@ function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
});
if (mergeRequestWidgetGraphql) {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } });
}
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 7082a19a8e7..f4ecebbb40c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -192,6 +192,8 @@ describe('ReadyToMerge', () => {
it('should return "Merge in progress"', async () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isMergingImmediately: true });
await Vue.nextTick();
@@ -260,6 +262,8 @@ describe('ReadyToMerge', () => {
it('should return true when the vm instance is making request', async () => {
createComponent({ mr: { isMergeAllowed: true } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isMakingRequest: true });
await Vue.nextTick();
@@ -287,6 +291,8 @@ describe('ReadyToMerge', () => {
jest
.spyOn(wrapper.vm.service, 'merge')
.mockReturnValue(returnPromise('merge_when_pipeline_succeeds'));
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ removeSourceBranch: false });
wrapper.vm.handleMergeButtonClick(true);
@@ -691,6 +697,8 @@ describe('ReadyToMerge', () => {
true,
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
loading: false,
state: {
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
index ae280146c22..8e46af5dfd6 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
@@ -1,6 +1,6 @@
export const invalidPlanWithName = {
job_name: 'Invalid Plan',
- job_path: '/path/to/ci/logs/1',
+ job_path: '/path/to/ci/logs/3',
tf_report_error: 'api_error',
};
@@ -20,12 +20,12 @@ export const validPlanWithoutName = {
create: 10,
update: 20,
delete: 30,
- job_path: '/path/to/ci/logs/1',
+ job_path: '/path/to/ci/logs/2',
};
export const plans = {
invalid_plan_one: invalidPlanWithName,
- invalid_plan_two: invalidPlanWithName,
+ invalid_plan_two: invalidPlanWithoutName,
valid_plan_one: validPlanWithName,
valid_plan_two: validPlanWithoutName,
};
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index 364f849eb4f..9048975875a 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -43,6 +43,8 @@ describe('MrWidgetTerraformConainer', () => {
mockPollingApi(200, plans, {});
return mountWrapper().then(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
new file mode 100644
index 00000000000..f8ea6fc23a2
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
@@ -0,0 +1,178 @@
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+import terraformExtension from '~/vue_merge_request_widget/extensions/terraform';
+import {
+ plans,
+ validPlanWithName,
+ validPlanWithoutName,
+ invalidPlanWithName,
+ invalidPlanWithoutName,
+} from '../../components/terraform/mock_data';
+
+describe('Terraform extension', () => {
+ let wrapper;
+ let mock;
+
+ const endpoint = '/path/to/terraform/report.json';
+ const successStatusCode = 200;
+ const errorStatusCode = 500;
+
+ const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
+
+ registerExtension(terraformExtension);
+
+ const mockPollingApi = (response, body, header) => {
+ mock.onGet(endpoint).reply(response, body, header);
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(extensionsContainer, {
+ propsData: {
+ mr: {
+ terraformReportsPath: endpoint,
+ },
+ },
+ });
+ return axios.waitForAll();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('summary', () => {
+ describe('while loading', () => {
+ const loadingText = 'Loading Terraform reports...';
+ it('should render loading text', async () => {
+ mockPollingApi(successStatusCode, plans, {});
+ createComponent();
+
+ expect(wrapper.text()).toContain(loadingText);
+ await waitForPromises();
+ expect(wrapper.text()).not.toContain(loadingText);
+ });
+ });
+
+ describe('when the fetching fails', () => {
+ beforeEach(() => {
+ mockPollingApi(errorStatusCode, null, {});
+ return createComponent();
+ });
+
+ it('should generate one invalid plan and render correct summary text', () => {
+ expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ });
+ });
+
+ describe('when the fetching succeeds', () => {
+ describe.each`
+ responseType | response | summaryTitle | summarySubtitle
+ ${'1 invalid report'} | ${{ 0: invalidPlanWithName }} | ${'1 Terraform report failed to generate'} | ${''}
+ ${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''}
+ ${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
+ `('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
+ beforeEach(async () => {
+ mockPollingApi(successStatusCode, response, {});
+ return createComponent();
+ });
+
+ it(`should render correct summary text`, () => {
+ expect(wrapper.text()).toContain(summaryTitle);
+
+ if (summarySubtitle) {
+ expect(wrapper.text()).toContain(summarySubtitle);
+ }
+ });
+ });
+ });
+ });
+
+ describe('expanded data', () => {
+ beforeEach(async () => {
+ mockPollingApi(successStatusCode, plans, {});
+ await createComponent();
+
+ wrapper.findByTestId('toggle-button').trigger('click');
+ });
+
+ describe.each`
+ reportType | title | subtitle | logLink | lineNumber
+ ${'a valid report with name'} | ${`The job ${validPlanWithName.job_name} generated a report.`} | ${`Reported Resource Changes: ${validPlanWithName.create} to add, ${validPlanWithName.update} to change, ${validPlanWithName.delete} to delete`} | ${validPlanWithName.job_path} | ${0}
+ ${'a valid report without name'} | ${'A Terraform report was generated in your pipelines.'} | ${`Reported Resource Changes: ${validPlanWithoutName.create} to add, ${validPlanWithoutName.update} to change, ${validPlanWithoutName.delete} to delete`} | ${validPlanWithoutName.job_path} | ${1}
+ ${'an invalid report with name'} | ${`The job ${invalidPlanWithName.job_name} failed to generate a report.`} | ${'Generating the report caused an error.'} | ${invalidPlanWithName.job_path} | ${2}
+ ${'an invalid report without name'} | ${'A Terraform report failed to generate.'} | ${'Generating the report caused an error.'} | ${invalidPlanWithoutName.job_path} | ${3}
+ `('renders correct text for $reportType', ({ title, subtitle, logLink, lineNumber }) => {
+ it('renders correct text', () => {
+ expect(findListItem(lineNumber).text()).toContain(title);
+ expect(findListItem(lineNumber).text()).toContain(subtitle);
+ });
+
+ it(`${logLink ? 'renders' : "doesn't render"} the log link`, () => {
+ const logText = 'Full log';
+ if (logLink) {
+ expect(
+ findListItem(lineNumber)
+ .find('[data-testid="extension-actions-button"]')
+ .attributes('href'),
+ ).toBe(logLink);
+ } else {
+ expect(findListItem(lineNumber).text()).not.toContain(logText);
+ }
+ });
+ });
+ });
+
+ describe('polling', () => {
+ let pollRequest;
+ let pollStop;
+
+ beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+ pollStop = jest.spyOn(Poll.prototype, 'stop');
+ });
+
+ afterEach(() => {
+ pollRequest.mockRestore();
+ pollStop.mockRestore();
+ });
+
+ describe('successful poll', () => {
+ beforeEach(() => {
+ mockPollingApi(successStatusCode, plans, {});
+
+ return createComponent();
+ });
+
+ it('does not make additional requests after poll is successful', () => {
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('polling fails', () => {
+ beforeEach(() => {
+ mockPollingApi(errorStatusCode, null, {});
+ return createComponent();
+ });
+
+ it('generates one broken plan', () => {
+ expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ });
+
+ it('does not make additional requests after poll is unsuccessful', () => {
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 4538c1320d0..20d00a116bb 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -282,6 +282,8 @@ export default {
gitpod_enabled: true,
show_gitpod_button: true,
gitpod_url: 'http://gitpod.localhost',
+ user_preferences_gitpod_path: '/-/profile/preferences#user_gitpod_enabled',
+ user_profile_enable_gitpod_path: '/-/profile?user%5Bgitpod_enabled%5D=true',
};
export const mockStore = {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 8d41f6620ff..56c9bae0b76 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -9,6 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
import SmartInterval from '~/smart_interval';
@@ -28,6 +29,8 @@ import {
workingExtension,
collapsedDataErrorExtension,
fullDataErrorExtension,
+ pollingExtension,
+ pollingErrorExtension,
} from './test_extensions';
jest.mock('~/api.js');
@@ -897,13 +900,19 @@ describe('MrWidgetOptions', () => {
});
describe('mock extension', () => {
+ let pollRequest;
+
beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+
registerExtension(workingExtension);
createComponent();
});
afterEach(() => {
+ pollRequest.mockRestore();
+
registeredExtensions.extensions = [];
});
@@ -957,6 +966,66 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.find(GlButton).exists()).toBe(true);
expect(collapsedSection.find(GlButton).text()).toBe('Full report');
});
+
+ it('extension polling is not called if enablePolling flag is not passed', () => {
+ // called one time due to parent component polling (mount)
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('mock polling extension', () => {
+ let pollRequest;
+ let pollStop;
+
+ beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+ pollStop = jest.spyOn(Poll.prototype, 'stop');
+ });
+
+ afterEach(() => {
+ pollRequest.mockRestore();
+ pollStop.mockRestore();
+
+ registeredExtensions.extensions = [];
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ registerExtension(pollingExtension);
+
+ createComponent();
+ });
+
+ it('does not make additional requests after poll is successful', () => {
+ // called two times due to parent component polling (mount) and extension polling
+ expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('error', () => {
+ let captureException;
+
+ beforeEach(() => {
+ captureException = jest.spyOn(Sentry, 'captureException');
+
+ registerExtension(pollingErrorExtension);
+
+ createComponent();
+ });
+
+ it('does not make additional requests after poll has failed', () => {
+ // called two times due to parent component polling (mount) and extension polling
+ expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+
+ it('captures sentry error and displays error when poll has failed', () => {
+ expect(captureException).toHaveBeenCalledTimes(1);
+ expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
+ });
+ });
});
describe('mock extension errors', () => {
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
index 6eb68a1b00d..3cdb4265ef0 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -15,6 +15,8 @@ describe('MergeRequestStore', () => {
gitpodEnabled: mockData.gitpod_enabled,
showGitpodButton: mockData.show_gitpod_button,
gitpodUrl: mockData.gitpod_url,
+ userPreferencesGitpodPath: mockData.user_preferences_gitpod_path,
+ userProfileEnableGitpodPath: mockData.user_profile_enable_gitpod_path,
});
});
diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_mr_widget/test_extensions.js
index c7ff02ab726..986c1d6545a 100644
--- a/spec/frontend/vue_mr_widget/test_extensions.js
+++ b/spec/frontend/vue_mr_widget/test_extensions.js
@@ -97,3 +97,13 @@ export const fullDataErrorExtension = {
},
},
};
+
+export const pollingExtension = {
+ ...workingExtension,
+ enablePolling: true,
+};
+
+export const pollingErrorExtension = {
+ ...collapsedDataErrorExtension,
+ enablePolling: true,
+};
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 1fc655f1ebc..221beed744b 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -349,6 +349,8 @@ describe('AlertDetails', () => {
${1} | ${'metrics'}
${2} | ${'activity'}
`('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentTabIndex: index });
expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 9ae45071f45..29e0eee2c9a 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -109,6 +109,8 @@ describe('Alert Details Sidebar Assignees', () => {
});
it('renders a unassigned option', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
await wrapper.vm.$nextTick();
expect(findDropdown().text()).toBe('Unassigned');
@@ -120,6 +122,8 @@ describe('Alert Details Sidebar Assignees', () => {
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
await wrapper.vm.$nextTick();
@@ -136,6 +140,8 @@ describe('Alert Details Sidebar Assignees', () => {
});
it('emits an error when request contains error messages', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
const errorMutationResult = {
data: {
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
index 530d01402c6..083a5f60d1d 100644
--- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
+++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
@@ -315,6 +315,8 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('passes updated prop via v-model', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ value: MOCK_VALUE });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 33445923a49..fca5e664a96 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,8 +1,16 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import initCopyToClipboard from '~/behaviors/copy_to_clipboard';
+import { nextTick } from 'vue';
+
+import initCopyToClipboard, {
+ CLIPBOARD_SUCCESS_EVENT,
+ CLIPBOARD_ERROR_EVENT,
+ I18N_ERROR_MESSAGE,
+} from '~/behaviors/copy_to_clipboard';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('clipboard button', () => {
let wrapper;
@@ -15,6 +23,42 @@ describe('clipboard button', () => {
const findButton = () => wrapper.find(GlButton);
+ const expectConfirmationTooltip = async ({ event, message }) => {
+ const title = 'Copy this value';
+
+ createWrapper({
+ text: 'copy me',
+ title,
+ });
+
+ wrapper.vm.$root.$emit = jest.fn();
+
+ const button = findButton();
+
+ expect(button.attributes()).toMatchObject({
+ title,
+ 'aria-label': title,
+ });
+
+ await button.trigger(event);
+
+ expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1');
+
+ expect(button.attributes()).toMatchObject({
+ title: message,
+ 'aria-label': message,
+ });
+
+ jest.runAllTimers();
+ await nextTick();
+
+ expect(button.attributes()).toMatchObject({
+ title,
+ 'aria-label': title,
+ });
+ expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
+ };
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -99,6 +143,32 @@ describe('clipboard button', () => {
expect(findButton().props('variant')).toBe(variant);
});
+ describe('confirmation tooltip', () => {
+ it('adds `id` and `data-clipboard-handle-tooltip` attributes to button', () => {
+ createWrapper({
+ text: 'copy me',
+ title: 'Copy this value',
+ });
+
+ expect(findButton().attributes()).toMatchObject({
+ id: 'clipboard-button-1',
+ 'data-clipboard-handle-tooltip': 'false',
+ 'aria-live': 'polite',
+ });
+ });
+
+ it('shows success tooltip after successful copy', () => {
+ expectConfirmationTooltip({
+ event: CLIPBOARD_SUCCESS_EVENT,
+ message: ClipboardButton.i18n.copied,
+ });
+ });
+
+ it('shows error tooltip after failed copy', () => {
+ expectConfirmationTooltip({ event: CLIPBOARD_ERROR_EVENT, message: I18N_ERROR_MESSAGE });
+ });
+ });
+
describe('integration', () => {
it('actually copies to clipboard', () => {
initCopyToClipboard();
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
index af7f85769aa..a179afccae0 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -10,6 +10,7 @@ describe('Confirm Danger Modal', () => {
const phrase = 'En Taro Adun';
const buttonText = 'Click me!';
const buttonClass = 'gl-w-full';
+ const buttonVariant = 'info';
const modalId = CONFIRM_DANGER_MODAL_ID;
const findBtn = () => wrapper.findComponent(GlButton);
@@ -21,6 +22,7 @@ describe('Confirm Danger Modal', () => {
propsData: {
buttonText,
buttonClass,
+ buttonVariant,
phrase,
...props,
},
@@ -57,6 +59,10 @@ describe('Confirm Danger Modal', () => {
expect(findBtn().classes()).toContain(buttonClass);
});
+ it('passes `buttonVariant` prop to button', () => {
+ expect(findBtn().attributes('variant')).toBe(buttonVariant);
+ });
+
it('will emit `confirm` when the modal confirms', () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 64d15884333..4e9eac2dde2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -122,6 +122,8 @@ describe('FilteredSearchBarRoot', () => {
describe('sortDirectionIcon', () => {
it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.ascending,
});
@@ -130,6 +132,8 @@ describe('FilteredSearchBarRoot', () => {
});
it('returns string "sort-highest" when `selectedSortDirection` is "descending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.descending,
});
@@ -140,6 +144,8 @@ describe('FilteredSearchBarRoot', () => {
describe('sortDirectionTooltip', () => {
it('returns string "Sort direction: Ascending" when `selectedSortDirection` is "ascending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.ascending,
});
@@ -148,6 +154,8 @@ describe('FilteredSearchBarRoot', () => {
});
it('returns string "Sort direction: Descending" when `selectedSortDirection` is "descending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.descending,
});
@@ -158,6 +166,8 @@ describe('FilteredSearchBarRoot', () => {
describe('filteredRecentSearches', () => {
it('returns array of recent searches filtering out any string type (unsupported) items', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
recentSearches: [{ foo: 'bar' }, 'foo'],
});
@@ -169,6 +179,8 @@ describe('FilteredSearchBarRoot', () => {
});
it('returns array of recent searches sanitizing any duplicate token values', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
recentSearches: [
[tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValueLabel],
@@ -198,6 +210,8 @@ describe('FilteredSearchBarRoot', () => {
describe('filterValue', () => {
it('emits component event `onFilter` with empty array and false when filter was never selected', () => {
wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
initialRender: false,
filterValue: [tokenValueEmpty],
@@ -210,6 +224,8 @@ describe('FilteredSearchBarRoot', () => {
it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => {
wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
initialRender: false,
filterValue: [tokenValueEmpty],
@@ -264,6 +280,8 @@ describe('FilteredSearchBarRoot', () => {
describe('handleSortDirectionClick', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
});
@@ -312,6 +330,8 @@ describe('FilteredSearchBarRoot', () => {
const mockFilters = [tokenValueAuthor, 'foo'];
beforeEach(async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
filterValue: mockFilters,
});
@@ -376,6 +396,8 @@ describe('FilteredSearchBarRoot', () => {
describe('template', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
selectedSortDirection: SortDirection.descending,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index b29c394e7ae..5865c6a41b8 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -10,10 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import {
- DEFAULT_LABEL_ANY,
- DEFAULT_NONE_ANY,
-} from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -227,6 +224,8 @@ describe('AuthorToken', () => {
expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
authors: [
{
@@ -274,7 +273,7 @@ describe('AuthorToken', () => {
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => {
+ it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors },
@@ -285,8 +284,9 @@ describe('AuthorToken', () => {
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(1 + currentUserLength);
- expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_ANY.text);
+ expect(suggestions).toHaveLength(2 + currentUserLength);
+ expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text);
+ expect(suggestions.at(1).text()).toBe(DEFAULT_NONE_ANY[1].text);
});
it('emits listeners in the base-token', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index f3e8b2d0c1b..cd8be765fb5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -121,6 +121,8 @@ describe('BranchToken', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: mockBranches[0].name } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
branches: mockBranches,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 36071c900df..ed9ac7c271e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -123,6 +123,8 @@ describe('EmojiToken', () => {
value: { data: `"${mockEmojis[0].name}"` },
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
emojis: mockEmojis,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index f55fb2836e3..b9af71ad8a7 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -144,6 +144,8 @@ describe('LabelToken', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labels: mockLabels,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index 4a098db33c5..c0d8b5fd139 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -121,6 +121,8 @@ describe('MilestoneToken', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
milestones: mockMilestones,
});
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
new file mode 100644
index 00000000000..b673e5407d4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -0,0 +1,77 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import flushPromises from 'helpers/flush_promises';
+import axios from '~/lib/utils/axios_utils';
+import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
+
+describe('GitlabVersionCheck', () => {
+ let wrapper;
+ let mock;
+
+ const defaultResponse = {
+ code: 200,
+ res: { severity: 'success' },
+ };
+
+ const createComponent = (mockResponse) => {
+ const response = {
+ ...defaultResponse,
+ ...mockResponse,
+ };
+
+ mock = new MockAdapter(axios);
+ mock.onGet().replyOnce(response.code, response.res);
+
+ wrapper = shallowMount(GitlabVersionCheck);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+
+ describe('template', () => {
+ describe.each`
+ description | mockResponse | renders
+ ${'successful but null'} | ${{ code: 200, res: null }} | ${false}
+ ${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
+ ${'an error'} | ${{ code: 500, res: null }} | ${false}
+ `('version_check.json response', ({ description, mockResponse, renders }) => {
+ describe(`is ${description}`, () => {
+ beforeEach(async () => {
+ createComponent(mockResponse);
+ await flushPromises(); // Ensure we wrap up the axios call
+ });
+
+ it(`does${renders ? '' : ' not'} render GlBadge`, () => {
+ expect(findGlBadge().exists()).toBe(renders);
+ });
+ });
+ });
+
+ describe.each`
+ mockResponse | expectedUI
+ ${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
+ ${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
+ ${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
+ `('badge ui', ({ mockResponse, expectedUI }) => {
+ describe(`when response is ${mockResponse.res.severity}`, () => {
+ beforeEach(async () => {
+ createComponent(mockResponse);
+ await flushPromises(); // Ensure we wrap up the axios call
+ });
+
+ it(`title is ${expectedUI.title}`, () => {
+ expect(findGlBadge().text()).toBe(expectedUI.title);
+ });
+
+ it(`variant is ${expectedUI.variant}`, () => {
+ expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js
index 5bedd0ccd02..38c26226863 100644
--- a/spec/frontend/vue_shared/components/line_numbers_spec.js
+++ b/spec/frontend/vue_shared/components/line_numbers_spec.js
@@ -13,7 +13,6 @@ describe('Line Numbers component', () => {
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findLineNumbers = () => wrapper.findAllComponents(GlLink);
const findFirstLineNumber = () => findLineNumbers().at(0);
- const findSecondLineNumber = () => findLineNumbers().at(1);
beforeEach(() => createComponent());
@@ -24,7 +23,7 @@ describe('Line Numbers component', () => {
expect(findLineNumbers().length).toBe(lines);
expect(findFirstLineNumber().attributes()).toMatchObject({
id: 'L1',
- href: '#L1',
+ to: '#LC1',
});
});
@@ -35,37 +34,4 @@ describe('Line Numbers component', () => {
});
});
});
-
- describe('clicking a line number', () => {
- let firstLineNumber;
- let firstLineNumberElement;
-
- beforeEach(() => {
- firstLineNumber = findFirstLineNumber();
- firstLineNumberElement = firstLineNumber.element;
-
- jest.spyOn(firstLineNumberElement, 'scrollIntoView');
- jest.spyOn(firstLineNumberElement.classList, 'add');
- jest.spyOn(firstLineNumberElement.classList, 'remove');
-
- firstLineNumber.vm.$emit('click');
- });
-
- it('adds the highlight (hll) class', () => {
- expect(firstLineNumberElement.classList.add).toHaveBeenCalledWith('hll');
- });
-
- it('removes the highlight (hll) class from a previously highlighted line', () => {
- findSecondLineNumber().vm.$emit('click');
-
- expect(firstLineNumberElement.classList.remove).toHaveBeenCalledWith('hll');
- });
-
- it('scrolls the line into view', () => {
- expect(firstLineNumberElement.scrollIntoView).toHaveBeenCalledWith({
- behavior: 'smooth',
- block: 'center',
- });
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 76e1a1162ad..0d90ca7f1f6 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
@@ -242,6 +243,41 @@ describe('Markdown field component', () => {
expect(dropzoneSpy).toHaveBeenCalled();
});
+
+ describe('mentioning all users', () => {
+ const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`);
+
+ it('shows warning on mention of all users', async () => {
+ axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+
+ subject.setProps({ textareaValue: 'hello @all' });
+
+ await axios.waitFor(markdownPreviewPath).then(() => {
+ expect(subject.text()).toContain(
+ 'You are about to add 11 people to the discussion. They will all receive a notification.',
+ );
+ });
+ });
+
+ it('removes warning when all mention is removed', async () => {
+ axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+
+ subject.setProps({ textareaValue: 'hello @all' });
+
+ await axios.waitFor(markdownPreviewPath);
+
+ jest.spyOn(axios, 'post');
+
+ subject.setProps({ textareaValue: 'hello @allan' });
+
+ await nextTick();
+
+ expect(axios.post).not.toHaveBeenCalled();
+ expect(subject.text()).not.toContain(
+ 'You are about to add 11 people to the discussion. They will all receive a notification.',
+ );
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index acf97713885..b330b4f5657 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -313,6 +313,8 @@ describe('AlertManagementEmptyState', () => {
it('returns correctly applied filter search values', async () => {
const searchTerm = 'foo';
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchTerm,
});
@@ -330,6 +332,8 @@ describe('AlertManagementEmptyState', () => {
});
it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
authorUsername: 'foo',
searchTerm: 'bar',
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index 23cf6ef9785..e8d76991b90 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Package code instruction multiline to match the snapshot 1`] = `
<div>
<label
- for="instruction-input_3"
+ for="instruction-input_1"
>
foo_label
</label>
@@ -23,7 +23,7 @@ multiline text
exports[`Package code instruction single line to match the default snapshot 1`] = `
<div>
<label
- for="instruction-input_2"
+ for="instruction-input_1"
>
foo_label
</label>
@@ -37,7 +37,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]
<input
class="form-control gl-font-monospace"
data-testid="instruction-input"
- id="instruction-input_2"
+ id="instruction-input_1"
readonly="readonly"
type="text"
/>
@@ -47,9 +47,12 @@ exports[`Package code instruction single line to match the default snapshot 1`]
data-testid="instruction-button"
>
<button
- aria-label="Copy this value"
+ aria-label="Copy npm install command"
+ aria-live="polite"
class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-handle-tooltip="false"
data-clipboard-text="npm i @my-package"
+ id="clipboard-button-1"
title="Copy npm install command"
type="button"
>
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 4ec608aaf07..3a2ea263a05 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -3,6 +3,8 @@ import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('Package code instruction', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index a5a099d803a..5336ecc614c 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -68,6 +68,8 @@ describe('IssuableMoveDropdown', () => {
describe('searchKey', () => {
it('calls `fetchProjects` with value of the prop', async () => {
jest.spyOn(wrapper.vm, 'fetchProjects');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'foo',
});
@@ -143,6 +145,8 @@ describe('IssuableMoveDropdown', () => {
`(
'returns $returnValue when selectedProject and provided project param $title',
async ({ project, selectedProject, returnValue }) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject,
});
@@ -154,6 +158,8 @@ describe('IssuableMoveDropdown', () => {
);
it('returns false when selectedProject is null', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject: null,
});
@@ -206,6 +212,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projectsListLoading: true,
});
@@ -216,6 +224,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-dropdown-item components for available projects', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: mockProjects,
selectedProject: mockProjects[0],
@@ -234,6 +244,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders string "No matching results" when search does not yield any matches', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'foo',
});
@@ -241,6 +253,8 @@ describe('IssuableMoveDropdown', () => {
// Wait for `searchKey` watcher to run.
await wrapper.vm.$nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: [],
projectsListLoading: false,
@@ -254,6 +268,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders string "Failed to load projects" when loading projects list fails', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: [],
projectsListLoading: false,
@@ -273,6 +289,8 @@ describe('IssuableMoveDropdown', () => {
expect(moveButtonEl.text()).toBe('Move');
expect(moveButtonEl.attributes('disabled')).toBe('true');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject: mockProjects[0],
});
@@ -303,6 +321,8 @@ describe('IssuableMoveDropdown', () => {
});
it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projectItemClick: true,
});
@@ -326,6 +346,8 @@ describe('IssuableMoveDropdown', () => {
});
it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: mockProjects,
});
@@ -338,6 +360,8 @@ describe('IssuableMoveDropdown', () => {
});
it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject: mockProjects[0],
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
index 1fe85637a62..0eff6a1dace 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -43,6 +43,8 @@ describe('DropdownContentsCreateView', () => {
});
it('returns `true` when `labelCreateInProgress` is true', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labelTitle: 'Foo',
selectedColor: '#ff0000',
@@ -55,6 +57,8 @@ describe('DropdownContentsCreateView', () => {
});
it('returns `false` when label title and color is defined and create request is not already in progress', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labelTitle: 'Foo',
selectedColor: '#ff0000',
@@ -99,6 +103,8 @@ describe('DropdownContentsCreateView', () => {
describe('handleCreateClick', () => {
it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => {
jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labelTitle: 'Foo',
selectedColor: '#ff0000',
@@ -164,6 +170,8 @@ describe('DropdownContentsCreateView', () => {
});
it('renders color input element', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedColor: '#ff0000',
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 80b8edd28ba..93a0e2f75bb 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -63,6 +63,8 @@ describe('DropdownContentsLabelsView', () => {
describe('computed', () => {
describe('visibleLabels', () => {
it('returns matching labels filtered with `searchKey`', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'bug',
});
@@ -72,6 +74,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('returns matching labels with fuzzy filtering', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'bg',
});
@@ -82,6 +86,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('returns all labels when `searchKey` is empty', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: '',
});
@@ -100,6 +106,8 @@ describe('DropdownContentsLabelsView', () => {
`(
'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
async ({ searchKey, labels, returnValue }) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey,
});
@@ -161,6 +169,8 @@ describe('DropdownContentsLabelsView', () => {
describe('handleKeyDown', () => {
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -173,6 +183,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -185,6 +197,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('resets the search text when the Enter key is pressed', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
searchKey: 'bug',
@@ -201,6 +215,8 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => {
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 2,
});
@@ -220,6 +236,8 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `toggleDropdownContents` when Esc key is pressed', () => {
jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -233,6 +251,8 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => {
jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -320,6 +340,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 0,
});
@@ -332,6 +354,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders element containing "No matching results" when `searchKey` does not match with any label', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'abc',
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index d8491334b5d..3ceed670d77 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -9,6 +9,7 @@ import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
import {
+ mockRegularLabel,
mockSuggestedColors,
createLabelSuccessfulResponse,
workspaceLabelsQueryResponse,
@@ -25,8 +26,18 @@ const userRecoverableError = {
errors: ['Houston, we have a problem'],
};
+const titleTakenError = {
+ data: {
+ labelCreate: {
+ label: mockRegularLabel,
+ errors: ['Title has already been taken'],
+ },
+ },
+};
+
const createLabelSuccessHandler = jest.fn().mockResolvedValue(createLabelSuccessfulResponse);
const createLabelUserRecoverableErrorHandler = jest.fn().mockResolvedValue(userRecoverableError);
+const createLabelDuplicateErrorHandler = jest.fn().mockResolvedValue(titleTakenError);
const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('DropdownContentsCreateView', () => {
@@ -208,4 +219,17 @@ describe('DropdownContentsCreateView', () => {
expect(createFlash).toHaveBeenCalled();
});
+
+ it('displays error in alert if label title is already taken', async () => {
+ createComponent({ mutationHandler: createLabelDuplicateErrorHandler });
+ fillLabelAttributes();
+ await nextTick();
+
+ findCreateButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlAlert).text()).toEqual(
+ titleTakenError.data.labelCreate.errors[0],
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 6f5a4b7e613..7f6770e0bea 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -110,6 +110,19 @@ describe('DropdownContentsLabelsView', () => {
});
});
+ it('first item is highlighted when search is not empty', async () => {
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(workspaceLabelsQueryResponse),
+ searchKey: 'Label',
+ });
+ await makeObserverAppear();
+ await waitForPromises();
+ await nextTick();
+
+ expect(findLabelsList().exists()).toBe(true);
+ expect(findFirstLabel().attributes('active')).toBe('true');
+ });
+
it('when search returns 0 results', async () => {
createComponent({
queryHandler: jest.fn().mockResolvedValue({
diff --git a/spec/frontend/vue_shared/components/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer_spec.js
index 758068379de..094d8d42a47 100644
--- a/spec/frontend/vue_shared/components/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer_spec.js
@@ -1,27 +1,35 @@
import hljs from 'highlight.js/lib/core';
+import Vue, { nextTick } from 'vue';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer.vue';
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('highlight.js/lib/core');
+Vue.use(VueRouter);
+const router = new VueRouter();
describe('Source Viewer component', () => {
let wrapper;
const content = `// Some source code`;
- const highlightedContent = `<span data-testid='test-highlighted'>${content}</span>`;
+ const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const language = 'javascript';
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
const createComponent = async (props = {}) => {
- wrapper = shallowMountExtended(SourceViewer, { propsData: { content, language, ...props } });
+ wrapper = shallowMountExtended(SourceViewer, {
+ router,
+ propsData: { content, language, ...props },
+ });
await waitForPromises();
};
const findLineNumbers = () => wrapper.findComponent(LineNumbers);
const findHighlightedContent = () => wrapper.findByTestId('test-highlighted');
+ const findFirstLine = () => wrapper.find('#LC1');
beforeEach(() => createComponent());
@@ -56,4 +64,39 @@ describe('Source Viewer component', () => {
expect(findHighlightedContent().exists()).toBe(true);
});
});
+
+ describe('selecting a line', () => {
+ let firstLine;
+ let firstLineElement;
+
+ beforeEach(() => {
+ firstLine = findFirstLine();
+ firstLineElement = firstLine.element;
+
+ jest.spyOn(firstLineElement, 'scrollIntoView');
+ jest.spyOn(firstLineElement.classList, 'add');
+ jest.spyOn(firstLineElement.classList, 'remove');
+ });
+
+ it('adds the highlight (hll) class', async () => {
+ wrapper.vm.$router.push('#LC1');
+ await nextTick();
+
+ expect(firstLineElement.classList.add).toHaveBeenCalledWith('hll');
+ });
+
+ it('removes the highlight (hll) class from a previously highlighted line', async () => {
+ wrapper.vm.$router.push('#LC2');
+ await nextTick();
+
+ expect(firstLineElement.classList.remove).toHaveBeenCalledWith('hll');
+ });
+
+ it('scrolls the line into view', () => {
+ expect(firstLineElement.scrollIntoView).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 92938b2717f..659d93d6597 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,11 +1,18 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
+const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled';
+const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true';
const ACTION_EDIT = {
href: TEST_EDIT_URL,
@@ -54,21 +61,31 @@ const ACTION_GITPOD = {
};
const ACTION_GITPOD_ENABLE = {
...ACTION_GITPOD,
- href: '#modal-enable-gitpod',
+ href: undefined,
handle: expect.any(Function),
};
describe('Web IDE link component', () => {
let wrapper;
- function createComponent(props) {
- wrapper = shallowMount(WebIdeLink, {
+ function createComponent(props, mountFn = shallowMountExtended) {
+ wrapper = mountFn(WebIdeLink, {
propsData: {
editUrl: TEST_EDIT_URL,
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
...props,
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: `
+ <div>
+ <slot name="modal-title"></slot>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>`,
+ }),
+ },
});
}
@@ -78,6 +95,7 @@ describe('Web IDE link component', () => {
const findActionsButton = () => wrapper.find(ActionsButton);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+ const findModal = () => wrapper.findComponent(GlModal);
it.each([
{
@@ -97,19 +115,68 @@ describe('Web IDE link component', () => {
expectedActions: [ACTION_WEB_IDE_CONFIRM_FORK, ACTION_EDIT_CONFIRM_FORK],
},
{
- props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true },
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: true,
+ },
expectedActions: [ACTION_EDIT, ACTION_GITPOD],
},
{
- props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false },
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ gitpodEnabled: true,
+ },
+ expectedActions: [ACTION_EDIT],
+ },
+ {
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: true,
+ },
+ expectedActions: [ACTION_EDIT],
+ },
+ {
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ gitpodEnabled: true,
+ },
+ expectedActions: [ACTION_EDIT],
+ },
+ {
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ },
expectedActions: [ACTION_EDIT, ACTION_GITPOD_ENABLE],
},
{
- props: { showGitpodButton: true, gitpodEnabled: false },
+ props: {
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ },
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT, ACTION_GITPOD_ENABLE],
},
{
- props: { showEditButton: false, showGitpodButton: true, gitpodText: 'Test Gitpod' },
+ props: {
+ showEditButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodText: 'Test Gitpod',
+ },
expectedActions: [ACTION_WEB_IDE, { ...ACTION_GITPOD_ENABLE, text: 'Test Gitpod' }],
},
{
@@ -128,6 +195,8 @@ describe('Web IDE link component', () => {
showEditButton: false,
showWebIdeButton: true,
showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
gitpodEnabled: true,
});
});
@@ -174,7 +243,7 @@ describe('Web IDE link component', () => {
])(
'emits the correct event when an action handler is called',
async ({ props, expectedEventPayload }) => {
- createComponent({ ...props, needsToFork: true });
+ createComponent({ ...props, needsToFork: true, disableForkModal: true });
findActionsButton().props('actions')[0].handle();
@@ -182,4 +251,72 @@ describe('Web IDE link component', () => {
},
);
});
+
+ describe('when Gitpod is not enabled', () => {
+ it('renders closed modal to enable Gitpod', () => {
+ createComponent({
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ });
+
+ const modal = findModal();
+
+ expect(modal.exists()).toBe(true);
+ expect(modal.props()).toMatchObject({
+ visible: false,
+ modalId: 'enable-gitpod-modal',
+ size: 'sm',
+ title: WebIdeLink.i18n.modal.title,
+ actionCancel: {
+ text: WebIdeLink.i18n.modal.actionCancelText,
+ },
+ actionPrimary: {
+ text: WebIdeLink.i18n.modal.actionPrimaryText,
+ attributes: {
+ variant: 'confirm',
+ category: 'primary',
+ href: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ 'data-method': 'put',
+ },
+ },
+ });
+ });
+
+ it('opens modal when `Gitpod` action is clicked', async () => {
+ const gitpodText = 'Open in Gitpod';
+
+ createComponent(
+ {
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ gitpodText,
+ },
+ mountExtended,
+ );
+
+ findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
+
+ await nextTick();
+ await wrapper.findByRole('button', { name: gitpodText }).trigger('click');
+
+ expect(findModal().props('visible')).toBe(true);
+ });
+ });
+
+ describe('when Gitpod is enabled', () => {
+ it('does not render modal', () => {
+ createComponent({
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: true,
+ });
+
+ expect(findModal().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index d7d7f4edc3f..b3f94d0242a 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -38,6 +38,8 @@ describe('Error Tracking directive', () => {
label: 'Trackable Info',
};
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ trackingOptions });
const { category, action, label, property, value } = trackingOptions;
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 5979a65e3cd..14e93108447 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -98,6 +98,8 @@ describe('IssuableListRoot', () => {
await wrapper.vm.$nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
checkedIssuables,
});
@@ -111,6 +113,8 @@ describe('IssuableListRoot', () => {
describe('bulkEditIssuables', () => {
it('returns array of issuables which have `checked` set to true within checkedIssuables map', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
checkedIssuables: mockCheckedIssuables,
});
@@ -180,6 +184,8 @@ describe('IssuableListRoot', () => {
describe('issuableChecked', () => {
it('returns boolean value representing checked status of issuable item', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
checkedIssuables: {
[mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
index 8c22b67bdbe..5723e2da586 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
@@ -1,5 +1,6 @@
import { GlTab, GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { setLanguage } from 'helpers/locale_helper';
import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue';
@@ -27,10 +28,12 @@ describe('IssuableTabs', () => {
let wrapper;
beforeEach(() => {
+ setLanguage('en');
wrapper = createComponent();
});
afterEach(() => {
+ setLanguage(null);
wrapper.destroy();
});
@@ -71,7 +74,7 @@ describe('IssuableTabs', () => {
// Does not render `All` badge since it has an undefined count
expect(badges).toHaveLength(2);
- expect(badges.at(0).text()).toBe(`${mockIssuableListProps.tabCounts.opened}`);
+ expect(badges.at(0).text()).toBe('5,000');
expect(badges.at(1).text()).toBe(`${mockIssuableListProps.tabCounts.closed}`);
});
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index e2fa99f7cc9..cfc7937b412 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -133,7 +133,7 @@ export const mockTabs = [
];
export const mockTabCounts = {
- opened: 5,
+ opened: 5000,
closed: 0,
all: undefined,
};
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index 1fcf37a0477..cb418371760 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -84,6 +84,8 @@ describe('IssuableTitle', () => {
});
it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
stickyTitleVisible: true,
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 02795751f33..ea26b2b4fb3 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
@@ -15,6 +16,7 @@ Vue.use(VueApollo);
const WORK_ITEM_ID = '1';
describe('Work items root component', () => {
+ const mockUpdatedTitle = 'Updated title';
let wrapper;
let fakeApollo;
@@ -53,7 +55,6 @@ describe('Work items root component', () => {
it('updates the title when it is edited', async () => {
createComponent();
jest.spyOn(wrapper.vm.$apollo, 'mutate');
- const mockUpdatedTitle = 'Updated title';
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
@@ -91,4 +92,32 @@ describe('Work items root component', () => {
expect(findTitle().exists()).toBe(false);
});
+
+ describe('tracking', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks item title updates', async () => {
+ await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, {
+ action: 'updated_title',
+ category: 'workItems:show',
+ label: 'item_title',
+ property: '[type_work_item]',
+ });
+ });
+ });
});
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
index 27e8236d593..9f30c95edd5 100644
--- a/spec/graphql/mutations/ci/runner/delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe Mutations::Ci::Runner::Delete do
include GraphqlHelpers
- let_it_be(:user) { create(:user) }
let_it_be(:runner) { create(:ci_runner) }
+ let(:user) { create(:user) }
let(:current_ctx) { { current_user: user } }
let(:mutation_params) do
@@ -46,10 +46,10 @@ RSpec.describe Mutations::Ci::Runner::Delete do
end
context 'when user can delete owned runner' do
- let_it_be(:project) { create(:project, creator_id: user.id) }
- let_it_be(:project_runner, reload: true) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project_runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
- before_all do
+ before do
project.add_maintainer(user)
end
@@ -63,10 +63,10 @@ RSpec.describe Mutations::Ci::Runner::Delete do
end
context 'with more than one associated project' do
- let_it_be(:project2) { create(:project, creator_id: user.id) }
- let_it_be(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
+ let!(:project2) { create(:project, creator_id: user.id) }
+ let!(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
- before_all do
+ before do
project2.add_maintainer(user)
end
diff --git a/spec/graphql/mutations/clusters/agent_tokens/revoke_spec.rb b/spec/graphql/mutations/clusters/agent_tokens/revoke_spec.rb
new file mode 100644
index 00000000000..f5f4c0cefad
--- /dev/null
+++ b/spec/graphql/mutations/clusters/agent_tokens/revoke_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Clusters::AgentTokens::Revoke do
+ let_it_be(:token) { create(:cluster_agent_token) }
+ let_it_be(:user) { create(:user) }
+
+ let(:mutation) do
+ described_class.new(
+ object: double,
+ context: { current_user: user },
+ field: double
+ )
+ end
+
+ it { expect(described_class.graphql_name).to eq('ClusterAgentTokenRevoke') }
+ it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+
+ describe '#resolve' do
+ let(:global_id) { token.to_global_id }
+
+ subject { mutation.resolve(id: global_id) }
+
+ context 'user does not have permission' do
+ it 'does not revoke the token' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+
+ expect(token.reload).not_to be_revoked
+ end
+ end
+
+ context 'user has permission' do
+ before do
+ token.agent.project.add_maintainer(user)
+ end
+
+ it 'revokes the token' do
+ subject
+
+ expect(token.reload).to be_revoked
+ end
+
+ context 'supplied ID is invalid' do
+ let(:global_id) { token.id }
+
+ it 'raises a coercion error' do
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+
+ expect(token.reload).not_to be_revoked
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
index 0f05504d4f2..d17d11305b1 100644
--- a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
+++ b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Contacts::Create do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+ let(:group) { create(:group, :crm_enabled) }
let(:not_found_or_does_not_belong) { 'The specified organization was not found or does not belong to this group' }
let(:valid_params) do
attributes_for(:contact,
@@ -34,11 +34,11 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
end
context 'when the user has permission' do
- before_all do
+ before do
group.add_developer(user)
end
- context 'when the feature is disabled' do
+ context 'when the feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@@ -49,6 +49,15 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
end
end
+ context 'when crm_enabled is false' do
+ let(:group) { create(:group) }
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+
context 'when the params are invalid' do
it 'returns the validation error' do
valid_params[:first_name] = nil
diff --git a/spec/graphql/mutations/customer_relations/contacts/update_spec.rb b/spec/graphql/mutations/customer_relations/contacts/update_spec.rb
index 4f59de194fd..c8206eca442 100644
--- a/spec/graphql/mutations/customer_relations/contacts/update_spec.rb
+++ b/spec/graphql/mutations/customer_relations/contacts/update_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Contacts::Update do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let(:first_name) { 'Lionel' }
let(:last_name) { 'Smith' }
diff --git a/spec/graphql/mutations/customer_relations/organizations/create_spec.rb b/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
index 9be0f5d4289..ee78d2b16f6 100644
--- a/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
+++ b/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Organizations::Create do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let(:valid_params) do
attributes_for(:organization,
diff --git a/spec/graphql/mutations/customer_relations/organizations/update_spec.rb b/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
index e3aa8eafe0c..90fd7a0a9f1 100644
--- a/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
+++ b/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Organizations::Update do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let(:name) { 'GitLab' }
let(:default_rate) { 1000.to_f }
@@ -56,7 +56,7 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
expect(resolve_mutation[:organization]).to have_attributes(attributes)
end
- context 'when the feature is disabled' do
+ context 'when the feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@@ -66,6 +66,15 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
.with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
+
+ context 'when the feature is disabled' do
+ let_it_be(:group) { create(:group) }
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
end
end
diff --git a/spec/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..d41118b1812
--- /dev/null
+++ b/spec/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Issues::SetEscalationStatus do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue, reload: true) { create(:incident, project: project) }
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ let(:status) { :acknowledged }
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ describe '#resolve' do
+ let(:args) { { status: status } }
+ let(:mutated_issue) { result[:issue] }
+
+ subject(:result) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, **args) }
+
+ it_behaves_like 'permission level for issue mutation is correctly verified', true
+
+ context 'when the user can update the issue' do
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'permission level for issue mutation is correctly verified', true
+
+ context 'when the user can update the escalation status' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'returns the issue with the escalation policy' do
+ expect(mutated_issue).to eq(issue)
+ expect(mutated_issue.escalation_status.status_name).to eq(status)
+ expect(result[:errors]).to be_empty
+ end
+
+ it 'returns errors when issue update fails' do
+ issue.update_column(:author_id, nil)
+
+ expect(result[:errors]).not_to be_empty
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
+ end
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb b/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
index 6b8b88928d8..9b54d466681 100644
--- a/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
+++ b/spec/graphql/resolvers/clusters/agent_tokens_resolver_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
it { expect(described_class.type).to eq(Types::Clusters::AgentTokenType) }
it { expect(described_class.null).to be_truthy }
+ it { expect(described_class.arguments.keys).to contain_exactly('status') }
describe '#resolve' do
let(:agent) { create(:cluster_agent) }
@@ -23,6 +24,14 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
expect(subject).to eq([matching_token2, matching_token1])
end
+ context 'token status is specified' do
+ let!(:revoked_token) { create(:cluster_agent_token, :revoked, agent: agent) }
+
+ subject { resolve(described_class, obj: agent, ctx: ctx, args: { status: 'revoked' }) }
+
+ it { is_expected.to contain_exactly(revoked_token) }
+ end
+
context 'user does not have permission' do
let(:user) { create(:user, developer_projects: [agent.project]) }
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
index 3fcfa967452..9fe4c78f551 100644
--- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -62,24 +62,12 @@ RSpec.describe ResolvesPipelines do
context 'filtering by source' do
let_it_be(:source_pipeline) { create(:ci_pipeline, project: project, source: 'web') }
- context 'when `dast_view_scans` feature flag is disabled' do
- before do
- stub_feature_flags(dast_view_scans: false)
- end
-
- it 'does not filter by source' do
- expect(resolve_pipelines(source: 'web')).to contain_exactly(*all_pipelines, source_pipeline)
- end
+ it 'does filter by source' do
+ expect(resolve_pipelines(source: 'web')).to contain_exactly(source_pipeline)
end
- context 'when `dast_view_scans` feature flag is enabled' do
- it 'does filter by source' do
- expect(resolve_pipelines(source: 'web')).to contain_exactly(source_pipeline)
- end
-
- it 'returns all the pipelines' do
- expect(resolve_pipelines).to contain_exactly(*all_pipelines, source_pipeline)
- end
+ it 'returns all the pipelines' do
+ expect(resolve_pipelines).to contain_exactly(*all_pipelines, source_pipeline)
end
end
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index a931b0a3f77..1d0eac30a23 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -172,6 +172,28 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
+ context 'with draft argument' do
+ before do
+ merge_request_4.update!(title: MergeRequest.wip_title(merge_request_4.title))
+ end
+
+ context 'with draft: true argument' do
+ it 'takes one argument' do
+ result = resolve_mr(project, draft: true)
+
+ expect(result).to contain_exactly(merge_request_4)
+ end
+ end
+
+ context 'with draft: false argument' do
+ it 'takes one argument' do
+ result = resolve_mr(project, draft: false)
+
+ expect(result).not_to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_5, merge_request_6)
+ end
+ end
+ end
+
context 'with label argument' do
let_it_be(:label) { merge_request_6.labels.first }
let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) }
diff --git a/spec/graphql/resolvers/users/groups_resolver_spec.rb b/spec/graphql/resolvers/users/groups_resolver_spec.rb
index 0fdb6da5ae9..5ac7aac4898 100644
--- a/spec/graphql/resolvers/users/groups_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/groups_resolver_spec.rb
@@ -26,14 +26,6 @@ RSpec.describe Resolvers::Users::GroupsResolver do
public_maintainer_group.add_maintainer(user)
end
- context 'when paginatable_namespace_drop_down_for_project_creation feature flag is disabled' do
- before do
- stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
- end
-
- it { is_expected.to be_nil }
- end
-
context 'when resolver object is current user' do
context 'when permission is :create_projects' do
let(:group_arguments) { { permission_scope: :create_projects } }
diff --git a/spec/graphql/resolvers/work_items/types_resolver_spec.rb b/spec/graphql/resolvers/work_items/types_resolver_spec.rb
new file mode 100644
index 00000000000..b85989256b5
--- /dev/null
+++ b/spec/graphql/resolvers/work_items/types_resolver_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::WorkItems::TypesResolver do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before_all do
+ group.add_developer(current_user)
+ end
+
+ describe '#resolve' do
+ it 'returns all default work item types' do
+ result = resolve(described_class, obj: group)
+
+ expect(result.to_a).to match(WorkItems::Type.default.order_by_name_asc)
+ end
+ end
+end
diff --git a/spec/graphql/types/ci/config/config_type_spec.rb b/spec/graphql/types/ci/config/config_type_spec.rb
index edd190a4365..0012ae9f51f 100644
--- a/spec/graphql/types/ci/config/config_type_spec.rb
+++ b/spec/graphql/types/ci/config/config_type_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Types::Ci::Config::ConfigType do
mergedYaml
stages
status
+ warnings
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index e3cb56c2ad5..47d697ab8b8 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Types::Ci::JobType do
created_by_tag
detailedStatus
duration
+ downstreamPipeline
finished_at
id
manual_job
diff --git a/spec/graphql/types/ci/pipeline_message_type_spec.rb b/spec/graphql/types/ci/pipeline_message_type_spec.rb
new file mode 100644
index 00000000000..f5c20cd9bf6
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_message_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::PipelineMessageType do
+ specify { expect(described_class.graphql_name).to eq('PipelineMessage') }
+
+ it 'contains attributes related to a pipeline message' do
+ expected_fields = %w[
+ id content
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index 58724524785..94d1b42da37 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Types::Ci::PipelineType do
coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job job job_artifacts downstream
upstream path project active user_permissions warnings commit commit_path uses_needs
- test_report_summary test_suite ref
+ test_report_summary test_suite ref ref_path warning_messages
]
if Gitlab.ee?
diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb
index cf8650a4a03..43d8b585d6b 100644
--- a/spec/graphql/types/ci/runner_type_spec.rb
+++ b/spec/graphql/types/ci/runner_type_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe GitlabSchema.types['CiRunner'] do
it 'contains attributes related to a runner' do
expected_fields = %w[
- id description contacted_at maximum_timeout access_level active status
+ id description created_at contacted_at maximum_timeout access_level active status
version short_sha revision locked run_untagged ip_address runner_type tag_list
- project_count job_count admin_url user_permissions
+ project_count job_count admin_url edit_admin_url user_permissions executor_name
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/clusters/agent_token_status_enum_spec.rb b/spec/graphql/types/clusters/agent_token_status_enum_spec.rb
new file mode 100644
index 00000000000..071e4050cfb
--- /dev/null
+++ b/spec/graphql/types/clusters/agent_token_status_enum_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Clusters::AgentTokenStatusEnum do
+ it { expect(described_class.graphql_name).to eq('AgentTokenStatus') }
+ it { expect(described_class.values.keys).to match_array(Clusters::AgentToken.statuses.keys.map(&:upcase)) }
+end
diff --git a/spec/graphql/types/clusters/agent_token_type_spec.rb b/spec/graphql/types/clusters/agent_token_type_spec.rb
index c872d201fd9..3f0720cb4b5 100644
--- a/spec/graphql/types/clusters/agent_token_type_spec.rb
+++ b/spec/graphql/types/clusters/agent_token_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ClusterAgentToken'] do
- let(:fields) { %i[cluster_agent created_at created_by_user description id last_used_at name] }
+ let(:fields) { %i[cluster_agent created_at created_by_user description id last_used_at name status] }
it { expect(described_class.graphql_name).to eq('ClusterAgentToken') }
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index 2f74ce81761..c1d838c3117 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
:id, :sha, :short_id, :title, :full_title, :full_title_html, :description, :description_html, :message, :title_html, :authored_date,
- :author_name, :author_gravatar, :author, :web_url, :web_path,
+ :author_name, :author_email, :author_gravatar, :author, :web_url, :web_path,
:pipelines, :signature_html
)
end
diff --git a/spec/graphql/types/group_member_relation_enum_spec.rb b/spec/graphql/types/group_member_relation_enum_spec.rb
index 315809ef75e..89ee8c574c4 100644
--- a/spec/graphql/types/group_member_relation_enum_spec.rb
+++ b/spec/graphql/types/group_member_relation_enum_spec.rb
@@ -6,6 +6,6 @@ RSpec.describe Types::GroupMemberRelationEnum do
specify { expect(described_class.graphql_name).to eq('GroupMemberRelation') }
it 'exposes all the existing group member relation type values' do
- expect(described_class.values.keys).to contain_exactly('DIRECT', 'INHERITED', 'DESCENDANTS')
+ expect(described_class.values.keys).to contain_exactly('DIRECT', 'INHERITED', 'DESCENDANTS', 'SHARED_FROM_GROUPS')
end
end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index dca2c930eea..0ba322a100a 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe GitlabSchema.types['Group'] do
dependency_proxy_blobs dependency_proxy_image_count
dependency_proxy_blob_count dependency_proxy_total_size
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
- shared_runners_setting timelogs organizations contacts
+ shared_runners_setting timelogs organizations contacts work_item_types
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/incident_management/escalation_status_enum_spec.rb b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb
new file mode 100644
index 00000000000..b39d4d9324e
--- /dev/null
+++ b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['IssueEscalationStatus'] do
+ specify { expect(described_class.graphql_name).to eq('IssueEscalationStatus') }
+
+ describe 'statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status_name, :status_value) do
+ 'TRIGGERED' | :triggered
+ 'ACKNOWLEDGED' | :acknowledged
+ 'RESOLVED' | :resolved
+ 'IGNORED' | :ignored
+ 'INVALID' | nil
+ end
+
+ with_them do
+ it 'exposes a status with the correct value' do
+ expect(described_class.values[status_name]&.value).to eq(status_value)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 1b8bf007a73..1d4590cbb4e 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
- create_note_email timelogs project_id customer_relations_contacts]
+ create_note_email timelogs project_id customer_relations_contacts escalation_status]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
@@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do
end
end
end
+
+ describe 'escalation_status' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue, reload: true) { create(:issue, project: project) }
+
+ let(:execute) { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ issue(iid: "#{issue.iid}") {
+ escalationStatus
+ }
+ }
+ }
+ )
+ end
+
+ subject(:status) { execute.dig('data', 'project', 'issue', 'escalationStatus') }
+
+ it { is_expected.to be_nil }
+
+ context 'for an incident' do
+ before do
+ issue.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it { is_expected.to be_nil }
+
+ context 'with an escalation status record' do
+ let!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ it { is_expected.to eq(escalation_status.status_name.to_s.upcase) }
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index b17b7c32289..5ab8845246a 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
total_time_spent human_time_estimate human_total_time_spent reference author merged_at
commit_count current_user_todos conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
- has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
+ has_ci mergeable commits commits_without_merge_commits squash security_auto_fix default_squash_commit_message
auto_merge_strategy merge_user
]
@@ -133,4 +133,28 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
end
end
end
+
+ describe '#merge_user' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when MR is merged' do
+ let(:merge_request) { create(:merge_request, :with_merged_metrics, target_project: project, source_project: project) }
+
+ it 'is not nil' do
+ value = resolve_field(:merge_user, merge_request)
+
+ expect(value).not_to be_nil
+ end
+ end
+
+ context 'when MR is set to merge when pipeline succeeds' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, target_project: project, source_project: project) }
+
+ it 'is not nil' do
+ value = resolve_field(:merge_user, merge_request)
+
+ expect(value).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/mutation_type_spec.rb b/spec/graphql/types/mutation_type_spec.rb
index 95d835c88cf..1fc46f2d511 100644
--- a/spec/graphql/types/mutation_type_spec.rb
+++ b/spec/graphql/types/mutation_type_spec.rb
@@ -7,6 +7,14 @@ RSpec.describe Types::MutationType do
expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft)
end
+ describe 'deprecated mutations' do
+ describe 'clusterAgentTokenDelete' do
+ let(:field) { get_field('clusterAgentTokenDelete') }
+
+ it { expect(field.deprecation_reason).to eq('Tokens must be revoked with ClusterAgentTokenRevoke. Deprecated in 14.7.') }
+ end
+ end
+
def get_field(name)
described_class.fields[GraphqlHelpers.fieldnamerize(name)]
end
diff --git a/spec/graphql/types/packages/package_details_type_spec.rb b/spec/graphql/types/packages/package_details_type_spec.rb
index f0b684d6b07..ceeb000ff85 100644
--- a/spec/graphql/types/packages/package_details_type_spec.rb
+++ b/spec/graphql/types/packages/package_details_type_spec.rb
@@ -5,7 +5,10 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageDetailsType'] do
it 'includes all the package fields' do
expected_fields = %w[
- id name version created_at updated_at package_type tags project pipelines versions package_files dependency_links
+ id name version created_at updated_at package_type tags project
+ pipelines versions package_files dependency_links
+ npm_url maven_url conan_url nuget_url pypi_url pypi_setup_url
+ composer_url composer_config_repository_url
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index adf5507571b..cd216232569 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe GitlabSchema.types['Project'] do
container_repositories container_repositories_count
pipeline_analytics squash_read_only sast_ci_configuration
cluster_agent cluster_agents agent_configurations
- ci_template timelogs merge_commit_template squash_commit_template
+ ci_template timelogs merge_commit_template squash_commit_template work_item_types
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -289,6 +289,7 @@ RSpec.describe GitlabSchema.types['Project'] do
:source_branches,
:target_branches,
:state,
+ :draft,
:labels,
:before,
:after,
diff --git a/spec/graphql/types/projects/service_type_spec.rb b/spec/graphql/types/projects/service_type_spec.rb
index cb09f1ca6cc..0bffdfd629d 100644
--- a/spec/graphql/types/projects/service_type_spec.rb
+++ b/spec/graphql/types/projects/service_type_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Types::Projects::ServiceType do
describe ".resolve_type" do
it 'resolves the corresponding type for objects' do
expect(described_class.resolve_type(build(:jira_integration), {})).to eq(Types::Projects::Services::JiraServiceType)
- expect(described_class.resolve_type(build(:service), {})).to eq(Types::Projects::Services::BaseServiceType)
+ expect(described_class.resolve_type(build(:integration), {})).to eq(Types::Projects::Services::BaseServiceType)
expect(described_class.resolve_type(build(:drone_ci_integration), {})).to eq(Types::Projects::Services::BaseServiceType)
expect(described_class.resolve_type(build(:custom_issue_tracker_integration), {})).to eq(Types::Projects::Services::BaseServiceType)
end
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
index 21bc88e34c0..8d845e5d814 100644
--- a/spec/graphql/types/repository/blob_type_spec.rb
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -21,15 +21,21 @@ RSpec.describe Types::Repository::BlobType do
:file_type,
:edit_blob_path,
:stored_externally,
+ :external_storage,
:raw_path,
:replace_path,
:pipeline_editor_path,
+ :find_file_path,
+ :blame_path,
+ :history_path,
+ :permalink_path,
:code_owners,
:simple_viewer,
:rich_viewer,
:plain_data,
:can_modify_blob,
:can_current_user_push_to_branch,
+ :archived,
:ide_edit_path,
:external_storage_url,
:fork_and_edit_path,
diff --git a/spec/helpers/admin/background_migrations_helper_spec.rb b/spec/helpers/admin/background_migrations_helper_spec.rb
index 8880a00755b..9c1bb0b9c55 100644
--- a/spec/helpers/admin/background_migrations_helper_spec.rb
+++ b/spec/helpers/admin/background_migrations_helper_spec.rb
@@ -3,22 +3,22 @@
require "spec_helper"
RSpec.describe Admin::BackgroundMigrationsHelper do
- describe '#batched_migration_status_badge_class_name' do
+ describe '#batched_migration_status_badge_variant' do
using RSpec::Parameterized::TableSyntax
- where(:status, :class_name) do
- :active | 'badge-info'
- :paused | 'badge-warning'
- :failed | 'badge-danger'
- :finished | 'badge-success'
+ where(:status, :variant) do
+ :active | :info
+ :paused | :warning
+ :failed | :danger
+ :finished | :success
end
- subject { helper.batched_migration_status_badge_class_name(migration) }
+ subject { helper.batched_migration_status_badge_variant(migration) }
with_them do
let(:migration) { build(:batched_background_migration, status: status) }
- it { is_expected.to eq(class_name) }
+ it { is_expected.to eq(variant) }
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7390b9b3f58..8c2b4b16075 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -477,4 +477,44 @@ RSpec.describe ApplicationHelper do
expect(helper).to have_received(:form_for).with(user, expected_options)
end
end
+
+ describe '#page_class' do
+ context 'when logged_out_marketing_header experiment is enabled' do
+ let_it_be(:expected_class) { 'logged-out-marketing-header-candidate' }
+
+ let(:current_user) { nil }
+ let(:variant) { :candidate }
+
+ subject do
+ helper.page_class.flatten
+ end
+
+ before do
+ stub_experiments(logged_out_marketing_header: variant)
+ allow(helper).to receive(:current_user) { current_user }
+ end
+
+ context 'when candidate' do
+ it { is_expected.to include(expected_class) }
+ end
+
+ context 'when candidate (:trial_focused variant)' do
+ let(:variant) { :trial_focused }
+
+ it { is_expected.to include(expected_class) }
+ end
+
+ context 'when control' do
+ let(:variant) { :control }
+
+ it { is_expected.not_to include(expected_class) }
+ end
+
+ context 'when a user is logged in' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.not_to include(expected_class) }
+ end
+ end
+ end
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 3c2ac954fe5..e722f301522 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -253,6 +253,32 @@ RSpec.describe ApplicationSettingsHelper do
end
end
+ describe '.registration_features_can_be_prompted?' do
+ subject { helper.registration_features_can_be_prompted? }
+
+ before do
+ if Gitlab.ee?
+ allow(License).to receive(:current).and_return(nil)
+ end
+ end
+
+ context 'when service ping is enabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when service ping is disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe '#sidekiq_job_limiter_modes_for_select' do
subject { helper.sidekiq_job_limiter_modes_for_select }
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index b481c214ca1..4bb09699db4 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -312,12 +312,6 @@ RSpec.describe AuthHelper do
it { is_expected.to be_truthy }
end
- context 'when current user is set' do
- let(:user) { instance_double('User') }
-
- it { is_expected.to eq(false) }
- end
-
context 'when no key is set' do
before do
stub_config(extra: {})
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 4f060a0ae3b..1083faa5e19 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe AutoDevopsHelper do
context 'when another service is enabled' do
before do
- create(:service, project: project, category: :ci, active: true)
+ create(:integration, project: project, category: :ci, active: true)
end
it { is_expected.to eq(false) }
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index 5601ab2df2a..851e13d908f 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -167,6 +167,7 @@ RSpec.describe ButtonHelper do
expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
expect(element.attr('type')).to eq('button')
expect(element.attr('aria-label')).to eq('Copy')
+ expect(element.attr('aria-live')).to eq('polite')
expect(element.attr('data-toggle')).to eq('tooltip')
expect(element.attr('data-placement')).to eq('bottom')
expect(element.attr('data-container')).to eq('body')
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
index e5ef362e91b..489d9d3fcee 100644
--- a/spec/helpers/ci/jobs_helper_spec.rb
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe Ci::JobsHelper do
describe 'jobs data' do
let(:project) { create(:project, :repository) }
- let(:bridge) { create(:ci_bridge, status: :pending) }
+ let(:bridge) { create(:ci_bridge) }
- subject(:bridge_data) { helper.bridge_data(bridge) }
+ subject(:bridge_data) { helper.bridge_data(bridge, project) }
before do
allow(helper)
@@ -17,8 +17,10 @@ RSpec.describe Ci::JobsHelper do
it 'returns bridge data' do
expect(bridge_data).to eq({
- "build_name" => bridge.name,
- "empty-state-illustration-path" => '/path/to/illustration'
+ "build_id" => bridge.id,
+ "empty-state-illustration-path" => '/path/to/illustration',
+ "pipeline_iid" => bridge.pipeline.iid,
+ "project_full_path" => project.full_path
})
end
end
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index 874937bc4ce..b15569f03c7 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -46,6 +46,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"empty-state-illustration-path" => 'foo',
"initial-branch-name" => nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
+ "lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha),
@@ -72,6 +73,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"empty-state-illustration-path" => 'foo',
"initial-branch-name" => nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
+ "lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline_etag" => '',
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index 173a0d3ab3c..832b4da0e20 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Ci::RunnersHelper do
describe '#runner_status_icon', :clean_gitlab_redis_cache do
it "returns - not contacted yet" do
runner = create(:ci_runner)
- expect(helper.runner_status_icon(runner)).to include("not connected yet")
+ expect(helper.runner_status_icon(runner)).to include("not contacted yet")
end
it "returns offline text" do
@@ -79,12 +79,7 @@ RSpec.describe Ci::RunnersHelper do
it 'returns the data in format' do
expect(helper.admin_runners_data_attributes).to eq({
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
- registration_token: Gitlab::CurrentSettings.runners_registration_token,
- active_runners_count: '0',
- all_runners_count: '2',
- instance_runners_count: '1',
- group_runners_count: '0',
- project_runners_count: '1'
+ registration_token: Gitlab::CurrentSettings.runners_registration_token
})
end
end
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 49937a3b53a..8e5f38cd95a 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -21,6 +21,16 @@ RSpec.describe EnvironmentHelper do
expect(html).to have_css('a.ci-status.ci-success')
end
end
+
+ context 'for a blocked deployment' do
+ subject { helper.render_deployment_status(deployment) }
+
+ let(:deployment) { build(:deployment, :blocked) }
+
+ it 'indicates the status' do
+ expect(subject).to have_text('blocked')
+ end
+ end
end
describe '#environments_detail_data_json' do
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index aef240db5b8..38f06b19b94 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe EnvironmentsHelper do
it 'returns data' do
expect(metrics_data).to include(
- 'settings_path' => edit_project_service_path(project, 'prometheus'),
+ 'settings_path' => edit_project_integration_path(project, 'prometheus'),
'clusters_path' => project_clusters_path(project),
'metrics_dashboard_base_path' => environment_metrics_path(environment),
'current_environment_name' => environment.name,
diff --git a/spec/helpers/groups/crm_settings_helper_spec.rb b/spec/helpers/groups/crm_settings_helper_spec.rb
new file mode 100644
index 00000000000..6376cabda3a
--- /dev/null
+++ b/spec/helpers/groups/crm_settings_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::CrmSettingsHelper do
+ let_it_be(:group) { create(:group) }
+
+ describe '#crm_feature_flag_enabled?' do
+ subject do
+ helper.crm_feature_flag_enabled?(group)
+ end
+
+ context 'when feature flag is enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+end
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index 3b23d705790..bac73db5dd4 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe HooksHelper do
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook, project: project) }
+ let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
let(:system_hook) { create(:system_hook) }
describe '#link_to_test_hook' do
@@ -31,6 +32,15 @@ RSpec.describe HooksHelper do
end
end
+ context 'with a service hook' do
+ let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
+
+ it 'returns project-namespaced link' do
+ expect(helper.hook_log_path(project_hook, web_hook_log))
+ .to eq(web_hook_log.present.details_path)
+ end
+ end
+
context 'with a system hook' do
let(:web_hook_log) { create(:web_hook_log, web_hook: system_hook) }
diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb
index 3a7d4d12513..38ce17e34ba 100644
--- a/spec/helpers/integrations_helper_spec.rb
+++ b/spec/helpers/integrations_helper_spec.rb
@@ -20,6 +20,12 @@ RSpec.describe IntegrationsHelper do
end
describe '#integration_form_data' do
+ before do
+ allow(helper).to receive_messages(
+ request: double(referer: '/services')
+ )
+ end
+
let(:fields) do
[
:id,
@@ -39,7 +45,9 @@ RSpec.describe IntegrationsHelper do
:cancel_path,
:can_test,
:test_path,
- :reset_path
+ :reset_path,
+ :form_path,
+ :redirect_to
]
end
@@ -61,6 +69,10 @@ RSpec.describe IntegrationsHelper do
specify do
expect(subject[:reset_path]).to eq(helper.scoped_reset_integration_path(integration))
end
+
+ specify do
+ expect(subject[:redirect_to]).to eq('/services')
+ end
end
context 'Jira service' do
@@ -70,6 +82,20 @@ RSpec.describe IntegrationsHelper do
end
end
+ describe '#integration_overrides_data' do
+ let(:integration) { build_stubbed(:jira_integration) }
+ let(:fields) do
+ [
+ edit_path: edit_admin_application_settings_integration_path(integration),
+ overrides_path: overrides_admin_application_settings_integration_path(integration, format: :json)
+ ]
+ end
+
+ subject { helper.integration_overrides_data(integration) }
+
+ it { is_expected.to include(*fields) }
+ end
+
describe '#scoped_reset_integration_path' do
let(:integration) { build_stubbed(:jira_integration) }
let(:group) { nil }
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index ad0ea6911f1..065ac526ae4 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe IssuesHelper do
describe '#work_item_type_icon' do
it 'returns icon of all standard base types' do
- WorkItem::Type.base_types.each do |type|
+ WorkItems::Type.base_types.each do |type|
expect(work_item_type_icon(type[0])).to eq "issue-type-#{type[0].to_s.dasherize}"
end
end
@@ -246,27 +246,6 @@ RSpec.describe IssuesHelper do
end
end
- describe '#use_startup_call' do
- it 'returns false when a query param is present' do
- allow(controller.request).to receive(:query_parameters).and_return({ foo: 'bar' })
-
- expect(helper.use_startup_call?).to eq(false)
- end
-
- it 'returns false when user has stored sort preference' do
- controller.instance_variable_set(:@sort, 'updated_asc')
-
- expect(helper.use_startup_call?).to eq(false)
- end
-
- it 'returns true when request.query_parameters is empty with default sorting preference' do
- controller.instance_variable_set(:@sort, 'created_date')
- allow(controller.request).to receive(:query_parameters).and_return({})
-
- expect(helper.use_startup_call?).to eq(true)
- end
- end
-
describe '#issue_header_actions_data' do
let(:current_user) { create(:user) }
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
index 9d13fc65de7..ffc2bb31b8f 100644
--- a/spec/helpers/learn_gitlab_helper_spec.rb
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -176,6 +176,19 @@ RSpec.describe LearnGitlabHelper do
)
})
end
+
+ it 'calls experiment with expected context & options' do
+ allow(helper).to receive(:current_user).and_return(user)
+
+ expect(helper).to receive(:experiment).with(
+ :change_continuous_onboarding_link_urls,
+ namespace: namespace,
+ actor: user,
+ sticky_to: namespace
+ )
+
+ learn_gitlab_data
+ end
end
end
end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 6eb560e3f5c..00aa0fd1cba 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -188,44 +188,6 @@ RSpec.describe NamespacesHelper do
helper.namespaces_options
end
end
-
- describe 'include_groups_with_developer_maintainer_access parameter' do
- context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set for a project' do
- let!(:admin_project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
-
- it 'returns groups where user is a developer' do
- allow(helper).to receive(:current_user).and_return(user)
- stub_application_setting(default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
- admin_group.add_user(user, GroupMember::DEVELOPER)
-
- options = helper.namespaces_options_with_developer_maintainer_access
-
- expect(options).to include(admin_group.name)
- expect(options).not_to include(subgroup1.name)
- expect(options).to include(subgroup2.name)
- expect(options).not_to include(subgroup3.name)
- expect(options).to include(user_group.name)
- expect(options).to include(user.name)
- end
- end
-
- context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set globally' do
- it 'return groups where default is not overridden' do
- allow(helper).to receive(:current_user).and_return(user)
- stub_application_setting(default_project_creation: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
- admin_group.add_user(user, GroupMember::DEVELOPER)
-
- options = helper.namespaces_options_with_developer_maintainer_access
-
- expect(options).to include(admin_group.name)
- expect(options).to include(subgroup1.name)
- expect(options).to include(subgroup2.name)
- expect(options).not_to include(subgroup3.name)
- expect(options).to include(user_group.name)
- expect(options).to include(user.name)
- end
- end
- end
end
describe '#cascading_namespace_settings_popover_data' do
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index 10bd45e3189..ef6a6827826 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -20,7 +20,6 @@ RSpec.describe Nav::TopNavHelper do
let(:current_group) { nil }
let(:with_current_settings_admin_mode) { false }
let(:with_header_link_admin_mode) { false }
- let(:with_sherlock_enabled) { false }
let(:with_projects) { false }
let(:with_groups) { false }
let(:with_milestones) { false }
@@ -34,7 +33,6 @@ RSpec.describe Nav::TopNavHelper do
before do
allow(Gitlab::CurrentSettings).to receive(:admin_mode) { with_current_settings_admin_mode }
allow(helper).to receive(:header_link?).with(:admin_mode) { with_header_link_admin_mode }
- allow(Gitlab::Sherlock).to receive(:enabled?) { with_sherlock_enabled }
# Defaulting all `dashboard_nav_link?` calls to false ensures the EE-specific behavior
# is not enabled in this CE spec
@@ -434,27 +432,6 @@ RSpec.describe Nav::TopNavHelper do
expect(subject[:shortcuts]).to eq([expected_shortcuts])
end
end
-
- context 'when sherlock is enabled' do
- let(:with_sherlock_enabled) { true }
-
- before do
- # Note: We have to mock the sherlock route because the route is conditional on
- # sherlock being enabled, but it parsed at Rails load time and can't be overridden
- # in a spec.
- allow(helper).to receive(:sherlock_transactions_path) { '/fake_sherlock_path' }
- end
-
- it 'has sherlock as last :secondary item' do
- expected_sherlock_item = ::Gitlab::Nav::TopNavMenuItem.build(
- id: 'sherlock',
- title: 'Sherlock Transactions',
- icon: 'admin',
- href: '/fake_sherlock_path'
- )
- expect(subject[:secondary].last).to eq(expected_sherlock_item)
- end
- end
end
context 'when current_user is admin' do
diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb
index 1864f9fad15..857771ebba6 100644
--- a/spec/helpers/operations_helper_spec.rb
+++ b/spec/helpers/operations_helper_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe OperationsHelper do
expect(subject).to eq(
'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(project),
- 'prometheus_form_path' => project_service_path(project, prometheus_integration),
+ 'prometheus_form_path' => project_integration_path(project, prometheus_integration),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project),
'prometheus_authorization_key' => nil,
'prometheus_api_url' => nil,
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index 06c6cccd488..8b3c8411fbd 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -219,45 +219,4 @@ RSpec.describe PackagesHelper do
it { is_expected.to eq(expected_result) }
end
end
-
- describe '#package_details_data' do
- let_it_be(:package) { create(:package) }
-
- let(:expected_result) do
- {
- package_id: package.id,
- can_delete: 'true',
- project_name: project.name,
- group_list_url: ''
- }
- end
-
- before do
- allow(helper).to receive(:current_user) { project.owner }
- allow(helper).to receive(:can?) { true }
- end
-
- context 'in a project without a group' do
- it 'populates presenter data' do
- result = helper.package_details_data(project, package)
-
- expect(result).to match(hash_including(expected_result))
- end
- end
-
- context 'in a project with a group' do
- let_it_be(:group) { create(:group) }
- let_it_be(:project_with_group) { create(:project, group: group) }
-
- it 'populates presenter data' do
- result = helper.package_details_data(project_with_group, package)
- expected = expected_result.merge({
- group_list_url: group_packages_path(project_with_group.group),
- project_name: project_with_group.name
- })
-
- expect(result).to match(hash_including(expected))
- end
- end
- end
end
diff --git a/spec/helpers/projects/cluster_agents_helper_spec.rb b/spec/helpers/projects/cluster_agents_helper_spec.rb
index 2935a74586b..632544797ee 100644
--- a/spec/helpers/projects/cluster_agents_helper_spec.rb
+++ b/spec/helpers/projects/cluster_agents_helper_spec.rb
@@ -17,5 +17,10 @@ RSpec.describe Projects::ClusterAgentsHelper do
it 'returns project path' do
expect(subject[:project_path]).to eq(project.full_path)
end
+
+ it 'returns string contants' do
+ expect(subject[:activity_empty_state_image]).to be_kind_of(String)
+ expect(subject[:empty_state_svg_path]).to be_kind_of(String)
+ end
end
end
diff --git a/spec/helpers/projects/issues/service_desk_helper_spec.rb b/spec/helpers/projects/issues/service_desk_helper_spec.rb
deleted file mode 100644
index 05766ee13c6..00000000000
--- a/spec/helpers/projects/issues/service_desk_helper_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::Issues::ServiceDeskHelper do
- let_it_be(:project) { create(:project, :public, service_desk_enabled: true) }
-
- let(:user) { build_stubbed(:user) }
- let(:current_user) { user }
-
- describe '#service_desk_meta' do
- subject { helper.service_desk_meta(project) }
-
- context "when service desk is supported and user can edit project settings" do
- before do
- allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
- allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
- allow(helper).to receive(:current_user).and_return(user)
- allow(helper).to receive(:can?).with(current_user, :admin_project, project).and_return(true)
- end
-
- it {
- is_expected.to eq({
- is_service_desk_supported: true,
- is_service_desk_enabled: true,
- can_edit_project_settings: true,
- service_desk_address: project.service_desk_address,
- service_desk_help_page: help_page_path('user/project/service_desk'),
- edit_project_page: edit_project_path(project),
- svg_path: ActionController::Base.helpers.image_path('illustrations/service_desk_empty.svg')
- })
- }
- end
-
- context "when service desk is not supported and user cannot edit project settings" do
- before do
- allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(false)
- allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(false)
- allow(helper).to receive(:current_user).and_return(user)
- allow(helper).to receive(:can?).with(current_user, :admin_project, project).and_return(false)
- end
-
- it {
- is_expected.to eq({
- is_service_desk_supported: false,
- is_service_desk_enabled: false,
- can_edit_project_settings: false,
- incoming_email_help_page: help_page_path('administration/incoming_email', anchor: 'set-it-up'),
- svg_path: ActionController::Base.helpers.image_path('illustrations/service-desk-setup.svg')
- })
- }
- end
- end
-end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 17dcbab09bb..40cfdafc9ac 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe SearchHelper do
include MarkupHelper
+ include BadgesHelper
# Override simple_sanitize for our testing purposes
def simple_sanitize(str)
@@ -640,7 +641,7 @@ RSpec.describe SearchHelper do
}
},
{
- title: _('Last updated'),
+ title: _('Updated date'),
sortable: true,
sortParam: {
asc: 'updated_asc',
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index 12d791d8710..913be164a00 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe SnippetsHelper do
include Gitlab::Routing
include IconsHelper
+ include BadgesHelper
let_it_be(:public_personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:public_project_snippet) { create(:project_snippet, :public, :repository) }
@@ -72,7 +73,7 @@ RSpec.describe SnippetsHelper do
let(:visibility) { :private }
it 'returns the snippet badge' do
- expect(subject).to eq "<span class=\"badge badge-gray\">#{sprite_icon('lock', size: 14, css_class: 'gl-vertical-align-middle')} private</span>"
+ expect(subject).to eq gl_badge_tag('private', icon: 'lock')
end
end
diff --git a/spec/helpers/ssh_keys_helper_spec.rb b/spec/helpers/ssh_keys_helper_spec.rb
new file mode 100644
index 00000000000..1aa604f19be
--- /dev/null
+++ b/spec/helpers/ssh_keys_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SshKeysHelper do
+ describe '#ssh_key_allowed_algorithms' do
+ it 'returns string with the names of allowed algorithms that are quoted and joined by commas' do
+ allowed_algorithms = Gitlab::CurrentSettings.allowed_key_types.flat_map do |ssh_key_type_name|
+ Gitlab::SSHPublicKey.supported_algorithms_for_name(ssh_key_type_name)
+ end
+
+ quoted_allowed_algorithms = allowed_algorithms.map { |name| "'#{name}'" }
+
+ expected_string = Gitlab::Utils.to_exclusive_sentence(quoted_allowed_algorithms)
+
+ expect(ssh_key_allowed_algorithms).to eq(expected_string)
+ end
+
+ it 'returns only allowed algorithms' do
+ expect(ssh_key_allowed_algorithms).to match('ed25519')
+ stub_application_setting(ed25519_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
+ expect(ssh_key_allowed_algorithms).not_to match('ed25519')
+ end
+ end
+end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index bc25a2fcdfc..1a0ecd5d903 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -84,14 +84,21 @@ RSpec.describe TreeHelper do
describe '#web_ide_button_data' do
let(:blob) { project.repository.blob_at('refs/heads/master', @path) }
+ let_it_be(:user_preferences_gitpod_path) { '/-/profile/preferences#user_gitpod_enabled' }
+ let_it_be(:user_profile_enable_gitpod_path) { '/-/profile?user%5Bgitpod_enabled%5D=true' }
+
before do
@path = ''
@project = project
@ref = sha
- allow(helper).to receive(:current_user).and_return(nil)
- allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
- allow(helper).to receive(:can?).and_return(true)
+ allow(helper).to receive_messages(
+ current_user: nil,
+ can_collaborate_with_project?: true,
+ can?: true,
+ user_preferences_gitpod_path: user_preferences_gitpod_path,
+ user_profile_enable_gitpod_path: user_profile_enable_gitpod_path
+ )
end
subject { helper.web_ide_button_data(blob: blob) }
@@ -112,7 +119,10 @@ RSpec.describe TreeHelper do
edit_url: '',
web_ide_url: "/-/ide/project/#{project.full_path}/edit/#{sha}",
- gitpod_url: ''
+
+ gitpod_url: '',
+ user_preferences_gitpod_path: user_preferences_gitpod_path,
+ user_profile_enable_gitpod_path: user_profile_enable_gitpod_path
)
end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index bd52eda8a65..959c4a94a78 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -3,33 +3,34 @@
require 'spec_helper'
RSpec.describe VersionCheckHelper do
- describe '#version_status_badge' do
- it 'returns nil if not dev environment and not enabled' do
- stub_rails_env('development')
- allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
+ let_it_be(:user) { create(:user) }
- expect(helper.version_status_badge).to be(nil)
- end
-
- context 'when production and enabled' do
- before do
- stub_rails_env('production')
- allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true }
- allow(VersionCheck).to receive(:image_url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
+ describe '#show_version_check?' do
+ describe 'return conditions' do
+ where(:enabled, :consent, :is_admin, :result) do
+ [
+ [false, false, false, false],
+ [false, false, true, false],
+ [false, true, false, false],
+ [false, true, true, false],
+ [true, false, false, false],
+ [true, false, true, true],
+ [true, true, false, false],
+ [true, true, true, false]
+ ]
end
- it 'returns an image tag' do
- expect(helper.version_status_badge).to start_with('<img')
- end
-
- it 'has a js prefixed css class' do
- expect(helper.version_status_badge)
- .to match(/class="js-version-status-badge lazy"/)
- end
+ with_them do
+ before do
+ stub_application_setting(version_check_enabled: enabled)
+ allow(User).to receive(:single_user).and_return(double(user, requires_usage_stats_consent?: consent))
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(user).to receive(:can_read_all_resources?).and_return(is_admin)
+ end
- it 'has a VersionCheck image_url as the src' do
- expect(helper.version_status_badge)
- .to include(%{src="https://version.host.com/check.svg?gitlab_info=xxx"})
+ it 'returns correct results' do
+ expect(helper.show_version_check?).to eq result
+ end
end
end
end
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 164225a00b2..56e0a22d59f 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Doorkeeper.configuration do
before do
allow(controller).to receive(:current_user).and_return(current_user)
allow(controller).to receive(:session).and_return({})
- allow(controller).to receive(:request).and_return(OpenStruct.new(fullpath: '/return-path'))
+ allow(controller).to receive(:request).and_return(double('request', fullpath: '/return-path'))
allow(controller).to receive(:redirect_to)
allow(controller).to receive(:new_user_session_url).and_return('/login')
end
diff --git a/spec/initializers/session_store_spec.rb b/spec/initializers/session_store_spec.rb
index db90b335dc9..a94ce327a92 100644
--- a/spec/initializers/session_store_spec.rb
+++ b/spec/initializers/session_store_spec.rb
@@ -10,40 +10,10 @@ RSpec.describe 'Session initializer for GitLab' do
end
describe 'config#session_store' do
- context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is not set' do
- before do
- stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', nil)
- end
+ it 'initialized as a redis_store with a proper servers configuration' do
+ expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
- it 'initialized with Multistore as ENV var defaults to true' do
- expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
-
- load_session_store
- end
- end
-
- context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is disabled' do
- before do
- stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', false)
- end
-
- it 'initialized as a redis_store with a proper servers configuration' do
- expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(Redis::Store)))
-
- load_session_store
- end
- end
-
- context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is enabled' do
- before do
- stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', true)
- end
-
- it 'initialized as a redis_store with a proper servers configuration' do
- expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
-
- load_session_store
- end
+ load_session_store
end
end
end
diff --git a/spec/lib/api/entities/ci/pipeline_spec.rb b/spec/lib/api/entities/ci/pipeline_spec.rb
index 6a658cc3e18..2b8e59b68c6 100644
--- a/spec/lib/api/entities/ci/pipeline_spec.rb
+++ b/spec/lib/api/entities/ci/pipeline_spec.rb
@@ -3,14 +3,31 @@
require 'spec_helper'
RSpec.describe API::Entities::Ci::Pipeline do
- let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, user: user) }
let_it_be(:job) { create(:ci_build, name: "rspec", coverage: 30.212, pipeline: pipeline) }
let(:entity) { described_class.new(pipeline) }
subject { entity.as_json }
- it 'returns the coverage as a string' do
+ exposed_fields = %i[before_sha tag yaml_errors created_at updated_at started_at finished_at committed_at duration queued_duration]
+
+ exposed_fields.each do |field|
+ it "exposes pipeline #{field}" do
+ expect(subject[field]).to eq(pipeline.public_send(field))
+ end
+ end
+
+ it 'exposes pipeline user basic information' do
+ expect(subject[:user].keys).to include(:avatar_url, :web_url)
+ end
+
+ it 'exposes pipeline detailed status' do
+ expect(subject[:detailed_status].keys).to include(:icon, :favicon)
+ end
+
+ it 'exposes pipeline coverage as a string' do
expect(subject[:coverage]).to eq '30.21'
end
end
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb
index b9d6ab7a652..40f259b86e2 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
it 'includes basic fields' do
is_expected.to include(
draft: merge_request.draft?,
- work_in_progress: merge_request.draft?
+ work_in_progress: merge_request.draft?,
+ merge_user: nil
)
end
diff --git a/spec/lib/api/helpers/rate_limiter_spec.rb b/spec/lib/api/helpers/rate_limiter_spec.rb
new file mode 100644
index 00000000000..2fed1cf3604
--- /dev/null
+++ b/spec/lib/api/helpers/rate_limiter_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Helpers::RateLimiter do
+ let(:key) { :some_key }
+ let(:scope) { [:some, :scope] }
+ let(:request) { instance_double('Rack::Request') }
+ let(:user) { build_stubbed(:user) }
+
+ let(:api_class) do
+ Class.new do
+ include API::Helpers::RateLimiter
+
+ attr_reader :request, :current_user
+
+ def initialize(request, current_user)
+ @request = request
+ @current_user = current_user
+ end
+
+ def render_api_error!(**args)
+ end
+ end
+ end
+
+ subject { api_class.new(request, user) }
+
+ before do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?)
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:log_request)
+ end
+
+ describe '#check_rate_limit!' do
+ it 'calls ApplicationRateLimiter#throttled? with the right arguments' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false)
+ expect(subject).not_to receive(:render_api_error!)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+
+ it 'renders api error and logs request if throttled' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(true)
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user)
+ expect(subject).to receive(:render_api_error!).with({ error: _('This endpoint has been requested too many times. Try again later.') }, 429)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+
+ context 'when the bypass header is set' do
+ before do
+ allow(Gitlab::Throttle).to receive(:bypass_header).and_return('SOME_HEADER')
+ end
+
+ it 'skips rate limit if set to "1"' do
+ allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('1')
+
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+ expect(subject).not_to receive(:render_api_error!)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+
+ it 'does not skip rate limit if set to something else than "1"' do
+ allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('0')
+
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?)
+
+ subject.check_rate_limit!(key, scope: scope)
+ end
+ end
+ end
+end
diff --git a/spec/lib/backup/artifacts_spec.rb b/spec/lib/backup/artifacts_spec.rb
index 5a965030b01..102d787a5e1 100644
--- a/spec/lib/backup/artifacts_spec.rb
+++ b/spec/lib/backup/artifacts_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Backup::Artifacts do
Dir.mktmpdir do |tmpdir|
allow(JobArtifactUploader).to receive(:root) { "#{tmpdir}" }
- expect(backup.app_files_dir).to eq("#{tmpdir}")
+ expect(backup.app_files_dir).to eq("#{File.realpath(tmpdir)}")
end
end
end
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 92de191da2d..6bff0919293 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -134,7 +134,7 @@ RSpec.describe Backup::Files do
expect do
subject.dump
- end.to raise_error(/Backup operation failed:/)
+ end.to raise_error(/Failed to create compressed file/)
end
describe 'with STRATEGY=copy' do
@@ -170,7 +170,7 @@ RSpec.describe Backup::Files do
expect do
subject.dump
end.to output(/rsync failed/).to_stdout
- .and raise_error(/Backup failed/)
+ .and raise_error(/Failed to create compressed file/)
end
end
end
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index 2ccde517533..cd0d984fbdb 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Backup::GitalyBackup do
- let(:parallel) { nil }
- let(:parallel_storage) { nil }
+ let(:max_parallelism) { nil }
+ let(:storage_parallelism) { nil }
let(:progress) do
Tempfile.new('progress').tap do |progress|
@@ -23,7 +23,7 @@ RSpec.describe Backup::GitalyBackup do
progress.close
end
- subject { described_class.new(progress, parallel: parallel, parallel_storage: parallel_storage) }
+ subject { described_class.new(progress, max_parallelism: max_parallelism, storage_parallelism: storage_parallelism) }
context 'unknown' do
it 'fails to start unknown' do
@@ -48,7 +48,7 @@ RSpec.describe Backup::GitalyBackup do
subject.enqueue(project, Gitlab::GlRepository::DESIGN)
subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
- subject.wait
+ subject.finish!
expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle'))
expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki.bundle'))
@@ -58,24 +58,24 @@ RSpec.describe Backup::GitalyBackup do
end
context 'parallel option set' do
- let(:parallel) { 3 }
+ let(:max_parallelism) { 3 }
it 'passes parallel option through' do
expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-parallel', '3').and_call_original
subject.start(:create)
- subject.wait
+ subject.finish!
end
end
context 'parallel_storage option set' do
- let(:parallel_storage) { 3 }
+ let(:storage_parallelism) { 3 }
it 'passes parallel option through' do
expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-parallel-storage', '3').and_call_original
subject.start(:create)
- subject.wait
+ subject.finish!
end
end
@@ -83,7 +83,7 @@ RSpec.describe Backup::GitalyBackup do
expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false'))
subject.start(:create)
- expect { subject.wait }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1')
+ expect { subject.finish! }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1')
end
end
@@ -115,7 +115,7 @@ RSpec.describe Backup::GitalyBackup do
expect(Open3).to receive(:popen2).with(ssl_env, anything, 'create', '-path', anything).and_call_original
subject.start(:create)
- subject.wait
+ subject.finish!
end
end
end
@@ -145,7 +145,7 @@ RSpec.describe Backup::GitalyBackup do
subject.enqueue(project, Gitlab::GlRepository::DESIGN)
subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
- subject.wait
+ subject.finish!
collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) }
@@ -157,24 +157,24 @@ RSpec.describe Backup::GitalyBackup do
end
context 'parallel option set' do
- let(:parallel) { 3 }
+ let(:max_parallelism) { 3 }
it 'passes parallel option through' do
expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-parallel', '3').and_call_original
subject.start(:restore)
- subject.wait
+ subject.finish!
end
end
context 'parallel_storage option set' do
- let(:parallel_storage) { 3 }
+ let(:storage_parallelism) { 3 }
it 'passes parallel option through' do
expect(Open3).to receive(:popen2).with(expected_env, anything, 'restore', '-path', anything, '-parallel-storage', '3').and_call_original
subject.start(:restore)
- subject.wait
+ subject.finish!
end
end
@@ -182,7 +182,7 @@ RSpec.describe Backup::GitalyBackup do
expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false'))
subject.start(:restore)
- expect { subject.wait }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1')
+ expect { subject.finish! }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1')
end
end
end
diff --git a/spec/lib/backup/gitaly_rpc_backup_spec.rb b/spec/lib/backup/gitaly_rpc_backup_spec.rb
index fb442f4a86f..14f9d27ca6e 100644
--- a/spec/lib/backup/gitaly_rpc_backup_spec.rb
+++ b/spec/lib/backup/gitaly_rpc_backup_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Backup::GitalyRpcBackup do
subject.enqueue(project, Gitlab::GlRepository::DESIGN)
subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
- subject.wait
+ subject.finish!
expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle'))
expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki.bundle'))
@@ -52,7 +52,7 @@ RSpec.describe Backup::GitalyRpcBackup do
it 'logs an appropriate message', :aggregate_failures do
subject.start(:create)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
- subject.wait
+ subject.finish!
expect(progress).to have_received(:puts).with("[Failed] backing up #{project.full_path} (#{project.disk_path})")
expect(progress).to have_received(:puts).with("Error Fail in tests")
@@ -96,7 +96,7 @@ RSpec.describe Backup::GitalyRpcBackup do
subject.enqueue(project, Gitlab::GlRepository::DESIGN)
subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
- subject.wait
+ subject.finish!
collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) }
@@ -129,7 +129,7 @@ RSpec.describe Backup::GitalyRpcBackup do
subject.enqueue(project, Gitlab::GlRepository::DESIGN)
subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
- subject.wait
+ subject.finish!
end
context 'failure' do
@@ -143,7 +143,7 @@ RSpec.describe Backup::GitalyRpcBackup do
it 'logs an appropriate message', :aggregate_failures do
subject.start(:restore)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
- subject.wait
+ subject.finish!
expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})")
expect(progress).to have_received(:puts).with("Error Fail in tests")
diff --git a/spec/lib/backup/lfs_spec.rb b/spec/lib/backup/lfs_spec.rb
new file mode 100644
index 00000000000..fdc1c0c885d
--- /dev/null
+++ b/spec/lib/backup/lfs_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::Lfs do
+ let(:progress) { StringIO.new }
+
+ subject(:backup) { described_class.new(progress) }
+
+ describe '#dump' do
+ before do
+ allow(File).to receive(:realpath).and_call_original
+ allow(File).to receive(:realpath).with('/var/lfs-objects').and_return('/var/lfs-objects')
+ allow(File).to receive(:realpath).with('/var/lfs-objects/..').and_return('/var')
+ allow(Settings.lfs).to receive(:storage_path).and_return('/var/lfs-objects')
+ end
+
+ it 'uses the correct lfs dir in tar command', :aggregate_failures do
+ expect(backup.app_files_dir).to eq('/var/lfs-objects')
+ expect(backup).to receive(:tar).and_return('blabla-tar')
+ expect(backup).to receive(:run_pipeline!).with([%w(blabla-tar --exclude=lost+found -C /var/lfs-objects -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], ''])
+ expect(backup).to receive(:pipeline_succeeded?).and_return(true)
+
+ backup.dump
+ end
+ end
+end
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 32eea82cfdf..31cc3012eb1 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Backup::Manager do
end
describe '#pack' do
- let(:expected_backup_contents) { %w(repositories db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz backup_information.yml) }
+ let(:expected_backup_contents) { %w(repositories db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz terraform_state.tar.gz packages.tar.gz backup_information.yml) }
let(:tar_file) { '1546300800_2019_01_01_12.3_gitlab_backup.tar' }
let(:tar_system_options) { { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } }
let(:tar_cmdline) { ['tar', '-cf', '-', *expected_backup_contents, tar_system_options] }
@@ -57,7 +57,7 @@ RSpec.describe Backup::Manager do
end
context 'when skipped is set in backup_information.yml' do
- let(:expected_backup_contents) { %w{db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz backup_information.yml} }
+ let(:expected_backup_contents) { %w{db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz terraform_state.tar.gz packages.tar.gz backup_information.yml} }
let(:backup_information) do
{
backup_created_at: Time.zone.parse('2019-01-01'),
@@ -74,7 +74,7 @@ RSpec.describe Backup::Manager do
end
context 'when a directory does not exist' do
- let(:expected_backup_contents) { %w{db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz backup_information.yml} }
+ let(:expected_backup_contents) { %w{db uploads.tar.gz builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz terraform_state.tar.gz packages.tar.gz backup_information.yml} }
before do
expect(Dir).to receive(:exist?).with(File.join(Gitlab.config.backup.path, 'repositories')).and_return(false)
diff --git a/spec/lib/backup/object_backup_spec.rb b/spec/lib/backup/object_backup_spec.rb
new file mode 100644
index 00000000000..6192b5c3482
--- /dev/null
+++ b/spec/lib/backup/object_backup_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'backup object' do |setting|
+ let(:progress) { StringIO.new }
+ let(:backup_path) { "/var/#{setting}" }
+
+ subject(:backup) { described_class.new(progress) }
+
+ describe '#dump' do
+ before do
+ allow(File).to receive(:realpath).and_call_original
+ allow(File).to receive(:realpath).with(backup_path).and_return(backup_path)
+ allow(File).to receive(:realpath).with("#{backup_path}/..").and_return('/var')
+ allow(Settings.send(setting)).to receive(:storage_path).and_return(backup_path)
+ end
+
+ it 'uses the correct storage dir in tar command and excludes tmp', :aggregate_failures do
+ expect(backup.app_files_dir).to eq(backup_path)
+ expect(backup).to receive(:tar).and_return('blabla-tar')
+ expect(backup).to receive(:run_pipeline!).with([%W(blabla-tar --exclude=lost+found --exclude=./tmp -C #{backup_path} -cf - .), 'gzip -c -1'], any_args).and_return([[true, true], ''])
+ expect(backup).to receive(:pipeline_succeeded?).and_return(true)
+
+ backup.dump
+ end
+ end
+end
+
+RSpec.describe Backup::Packages do
+ it_behaves_like 'backup object', 'packages'
+end
+
+RSpec.describe Backup::TerraformState do
+ it_behaves_like 'backup object', 'terraform_state'
+end
diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb
index 85818038c9d..f3830da344b 100644
--- a/spec/lib/backup/repositories_spec.rb
+++ b/spec/lib/backup/repositories_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Backup::Repositories do
expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN)
expect(strategy).to have_received(:enqueue).with(project_snippet, Gitlab::GlRepository::SNIPPET)
expect(strategy).to have_received(:enqueue).with(personal_snippet, Gitlab::GlRepository::SNIPPET)
- expect(strategy).to have_received(:wait)
+ expect(strategy).to have_received(:finish!)
end
end
@@ -49,7 +49,7 @@ RSpec.describe Backup::Repositories do
projects.each do |project|
expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
- expect(strategy).to receive(:wait)
+ expect(strategy).to receive(:finish!)
subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
end
@@ -91,7 +91,7 @@ RSpec.describe Backup::Repositories do
projects.each do |project|
expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
- expect(strategy).to receive(:wait)
+ expect(strategy).to receive(:finish!)
subject.dump(max_concurrency: 2, max_storage_concurrency: 2)
end
@@ -114,7 +114,7 @@ RSpec.describe Backup::Repositories do
projects.each do |project|
expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
- expect(strategy).to receive(:wait)
+ expect(strategy).to receive(:finish!)
subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency)
end
@@ -128,7 +128,7 @@ RSpec.describe Backup::Repositories do
projects.each do |project|
expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
- expect(strategy).to receive(:wait)
+ expect(strategy).to receive(:finish!)
subject.dump(max_concurrency: 3, max_storage_concurrency: max_storage_concurrency)
end
@@ -184,7 +184,7 @@ RSpec.describe Backup::Repositories do
expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN)
expect(strategy).to have_received(:enqueue).with(project_snippet, Gitlab::GlRepository::SNIPPET)
expect(strategy).to have_received(:enqueue).with(personal_snippet, Gitlab::GlRepository::SNIPPET)
- expect(strategy).to have_received(:wait)
+ expect(strategy).to have_received(:finish!)
end
context 'restoring object pools' do
diff --git a/spec/lib/backup/repository_backup_error_spec.rb b/spec/lib/backup/repository_backup_error_spec.rb
deleted file mode 100644
index 44c75c1cf77..00000000000
--- a/spec/lib/backup/repository_backup_error_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Backup::RepositoryBackupError do
- let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
- let_it_be(:project) { create(:project, :repository) }
- let_it_be(:wiki) { ProjectWiki.new(project, nil ) }
-
- let(:backup_repos_path) { '/tmp/backup/repositories' }
-
- shared_examples 'includes backup path' do
- it { is_expected.to respond_to :container }
- it { is_expected.to respond_to :backup_repos_path }
-
- it 'expects exception message to include repo backup path location' do
- expect(subject.message).to include("#{subject.backup_repos_path}")
- end
-
- it 'expects exception message to include container being back-up' do
- expect(subject.message).to include("#{subject.container.disk_path}")
- end
- end
-
- context 'with snippet repository' do
- subject { described_class.new(snippet, backup_repos_path) }
-
- it_behaves_like 'includes backup path'
- end
-
- context 'with project repository' do
- subject { described_class.new(project, backup_repos_path) }
-
- it_behaves_like 'includes backup path'
- end
-
- context 'with wiki repository' do
- subject { described_class.new(wiki, backup_repos_path) }
-
- it_behaves_like 'includes backup path'
- end
-end
diff --git a/spec/lib/backup/uploads_spec.rb b/spec/lib/backup/uploads_spec.rb
index a82cb764f4d..c173916fe91 100644
--- a/spec/lib/backup/uploads_spec.rb
+++ b/spec/lib/backup/uploads_spec.rb
@@ -14,13 +14,14 @@ RSpec.describe Backup::Uploads do
allow(Gitlab.config.uploads).to receive(:storage_path) { tmpdir }
- expect(backup.app_files_dir).to eq("#{tmpdir}/uploads")
+ expect(backup.app_files_dir).to eq("#{File.realpath(tmpdir)}/uploads")
end
end
end
describe '#dump' do
before do
+ allow(File).to receive(:realpath).and_call_original
allow(File).to receive(:realpath).with('/var/uploads').and_return('/var/uploads')
allow(File).to receive(:realpath).with('/var/uploads/..').and_return('/var')
allow(Gitlab.config.uploads).to receive(:storage_path) { '/var' }
diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb
index d41f5e8633d..5ac7d3af733 100644
--- a/spec/lib/banzai/filter/footnote_filter_spec.rb
+++ b/spec/lib/banzai/filter/footnote_filter_spec.rb
@@ -56,52 +56,6 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
it 'properly adds the necessary ids and classes' do
expect(doc.to_html).to eq filtered_footnote.strip
end
-
- context 'using ruby-based HTML renderer' do
- # first[^1] and second[^second]
- # [^1]: one
- # [^second]: two
- let(:footnote) do
- <<~EOF
- <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p>
- <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p>
- <ol>
- <li id="fn1">
- <p>one <a href="#fnref1">↩</a></p>
- </li>
- <li id="fn2">
- <p>two <a href="#fnref2">↩</a></p>
- </li>
- </ol>
- EOF
- end
-
- let(:filtered_footnote) do
- <<~EOF
- <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p>
- <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p>
- <section class="footnotes"><ol>
- <li id="fn1-#{identifier}">
- <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p>
- </li>
- <li id="fn2-#{identifier}">
- <p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p>
- </li>
- </ol></section>
- EOF
- end
-
- let(:doc) { filter(footnote) }
- let(:identifier) { link_node[:id].delete_prefix('fnref1-') }
-
- before do
- stub_feature_flags(use_cmark_renderer: false)
- end
-
- it 'properly adds the necessary ids and classes' do
- expect(doc.to_html).to eq filtered_footnote
- end
- end
end
context 'when detecting footnotes' do
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index 1c9b894e885..e3c8d121587 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -5,125 +5,90 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper
- shared_examples_for 'renders correct markdown' do
- describe 'markdown engine from context' do
- it 'defaults to CommonMark' do
- expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
- expect(instance).to receive(:render).and_return('test')
- end
-
- filter('test')
+ describe 'markdown engine from context' do
+ it 'defaults to CommonMark' do
+ expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
+ expect(instance).to receive(:render).and_return('test')
end
- it 'uses CommonMark' do
- expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
- expect(instance).to receive(:render).and_return('test')
- end
+ filter('test')
+ end
- filter('test', { markdown_engine: :common_mark })
+ it 'uses CommonMark' do
+ expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
+ expect(instance).to receive(:render).and_return('test')
end
+
+ filter('test', { markdown_engine: :common_mark })
end
+ end
- describe 'code block' do
- context 'using CommonMark' do
- before do
- stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
- end
-
- it 'adds language to lang attribute when specified' do
- result = filter("```html\nsome code\n```", no_sourcepos: true)
-
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- expect(result).to start_with('<pre lang="html"><code>')
- else
- expect(result).to start_with('<pre><code lang="html">')
- end
- end
-
- it 'does not add language to lang attribute when not specified' do
- result = filter("```\nsome code\n```", no_sourcepos: true)
-
- expect(result).to start_with('<pre><code>')
- end
-
- it 'works with utf8 chars in language' do
- result = filter("```æ—¥\nsome code\n```", no_sourcepos: true)
-
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- expect(result).to start_with('<pre lang="æ—¥"><code>')
- else
- expect(result).to start_with('<pre><code lang="æ—¥">')
- end
- end
-
- it 'works with additional language parameters' do
- result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
-
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- expect(result).to start_with('<pre lang="ruby:red" data-meta="gem foo"><code>')
- else
- expect(result).to start_with('<pre><code lang="ruby:red gem foo">')
- end
- end
+ describe 'code block' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
end
- end
- describe 'source line position' do
- context 'using CommonMark' do
- before do
- stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
- end
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```", no_sourcepos: true)
- it 'defaults to add data-sourcepos' do
- result = filter('test')
+ expect(result).to start_with('<pre lang="html"><code>')
+ end
- expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
- end
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```", no_sourcepos: true)
- it 'disables data-sourcepos' do
- result = filter('test', no_sourcepos: true)
+ expect(result).to start_with('<pre><code>')
+ end
+
+ it 'works with utf8 chars in language' do
+ result = filter("```æ—¥\nsome code\n```", no_sourcepos: true)
- expect(result).to eq '<p>test</p>'
- end
+ expect(result).to start_with('<pre lang="æ—¥"><code>')
+ end
+
+ it 'works with additional language parameters' do
+ result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
+
+ expect(result).to start_with('<pre lang="ruby:red" data-meta="gem foo"><code>')
end
end
+ end
- describe 'footnotes in tables' do
- it 'processes footnotes in table cells' do
- text = <<-MD.strip_heredoc
- | Column1 |
- | --------- |
- | foot [^1] |
+ describe 'source line position' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ end
- [^1]: a footnote
- MD
+ it 'defaults to add data-sourcepos' do
+ result = filter('test')
- result = filter(text, no_sourcepos: true)
+ expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
+ end
- expect(result).to include('<td>foot <sup')
+ it 'disables data-sourcepos' do
+ result = filter('test', no_sourcepos: true)
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- expect(result).to include('<section class="footnotes" data-footnotes>')
- else
- expect(result).to include('<section class="footnotes">')
- end
+ expect(result).to eq '<p>test</p>'
end
end
end
- context 'using ruby-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: false)
- end
+ describe 'footnotes in tables' do
+ it 'processes footnotes in table cells' do
+ text = <<-MD.strip_heredoc
+ | Column1 |
+ | --------- |
+ | foot [^1] |
- it_behaves_like 'renders correct markdown'
- end
+ [^1]: a footnote
+ MD
- context 'using c-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: true)
- end
+ result = filter(text, no_sourcepos: true)
- it_behaves_like 'renders correct markdown'
+ expect(result).to include('<td>foot <sup')
+ expect(result).to include('<section class="footnotes" data-footnotes>')
+ end
end
end
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index e1e02c09fbe..2d1a01116e0 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -5,67 +5,33 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper
- shared_examples_for 'renders correct markdown' do
- it 'replaces plantuml pre tag with img tag' do
- stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
+ it 'replaces plantuml pre tag with img tag' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
- input = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
- else
- '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
- end
+ input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
+ output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
+ doc = filter(input)
- output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
- doc = filter(input)
-
- expect(doc.to_s).to eq output
- end
-
- it 'does not replace plantuml pre tag with img tag if disabled' do
- stub_application_setting(plantuml_enabled: false)
-
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
- output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
- else
- input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
- output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
- end
-
- doc = filter(input)
-
- expect(doc.to_s).to eq output
- end
-
- it 'does not replace plantuml pre tag with img tag if url is invalid' do
- stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
+ expect(doc.to_s).to eq output
+ end
- input = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
- else
- '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
- end
+ it 'does not replace plantuml pre tag with img tag if disabled' do
+ stub_application_setting(plantuml_enabled: false)
- output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
- doc = filter(input)
+ input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
+ output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
+ doc = filter(input)
- expect(doc.to_s).to eq output
- end
+ expect(doc.to_s).to eq output
end
- context 'using ruby-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: false)
- end
-
- it_behaves_like 'renders correct markdown'
- end
+ it 'does not replace plantuml pre tag with img tag if url is invalid' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
- context 'using c-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: true)
- end
+ input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
+ output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
+ doc = filter(input)
- it_behaves_like 'renders correct markdown'
+ expect(doc.to_s).to eq output
end
end
diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index 14c1542b724..b3523a25116 100644
--- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -122,6 +122,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
+ expect(link.attr('href')).to eq(issue_url)
end
it 'includes a data-reference-format attribute for URL references' do
@@ -130,6 +131,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
+ expect(link.attr('href')).to eq(issue_url)
end
it 'supports an :only_path context' do
diff --git a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
index 3c488820853..e5809ac6949 100644
--- a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
@@ -51,6 +51,7 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
context 'internal reference' do
let(:reference) { merge.to_reference }
+ let(:merge_request_url) { urls.project_merge_request_url(project, merge) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@@ -115,14 +116,16 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
+ expect(link.attr('href')).to eq(merge_request_url)
end
it 'includes a data-reference-format attribute for URL references' do
- doc = reference_filter("Merge #{urls.project_merge_request_url(project, merge)}+")
+ doc = reference_filter("Merge #{merge_request_url}+")
link = doc.css('a').first
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
+ expect(link.attr('href')).to eq(merge_request_url)
end
it 'supports an :only_path context' do
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 24e787bddd5..039ca36af6e 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -177,53 +177,6 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
expect(act.to_html).to eq exp
end
end
-
- context 'using ruby-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: false)
- end
-
- it 'allows correct footnote id property on links' do
- exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>)
- act = filter(exp)
-
- expect(act.to_html).to eq exp
- end
-
- it 'allows correct footnote id property on li element' do
- exp = %q(<ol><li id="fn1">footnote</li></ol>)
- act = filter(exp)
-
- expect(act.to_html).to eq exp
- end
-
- it 'removes invalid id for footnote links' do
- exp = %q(<a href="#fn1">link</a>)
-
- %w[fnrefx test xfnref1].each do |id|
- act = filter(%(<a href="#fn1" id="#{id}">link</a>))
-
- expect(act.to_html).to eq exp
- end
- end
-
- it 'removes invalid id for footnote li' do
- exp = %q(<ol><li>footnote</li></ol>)
-
- %w[fnx test xfn1].each do |id|
- act = filter(%(<ol><li id="#{id}">footnote</li></ol>))
-
- expect(act.to_html).to eq exp
- end
- end
-
- it 'allows footnotes numbered higher than 9' do
- exp = %q(<a href="#fn15" id="fnref15">link</a><ol><li id="fn15">footnote</li></ol>)
- act = filter(exp)
-
- expect(act.to_html).to eq exp
- end
- end
end
end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index ef46fd62486..aee4bd93207 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -19,202 +19,150 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end
end
- shared_examples_for 'renders correct markdown' do
- context "when no language is specified" do
- it "highlights as plaintext" do
- result = filter('<pre><code>def fun end</code></pre>')
+ context "when no language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>')
- end
-
- include_examples "XSS prevention", ""
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>')
end
- context "when contains mermaid diagrams" do
- it "ignores mermaid blocks" do
- result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+ include_examples "XSS prevention", ""
+ end
- expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
- end
+ context "when contains mermaid diagrams" do
+ it "ignores mermaid blocks" do
+ result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+
+ expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
end
+ end
- context "when a valid language is specified" do
- it "highlights as that language" do
- result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- filter('<pre lang="ruby"><code>def fun end</code></pre>')
- else
- filter('<pre><code lang="ruby">def fun end</code></pre>')
- end
+ context "when <pre> contains multiple <code> tags" do
+ it "ignores the block" do
+ result = filter('<pre><code>one</code> and <code>two</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>')
- end
+ expect(result.to_html).to eq('<pre><code>one</code> and <code>two</code></pre>')
+ end
+ end
- include_examples "XSS prevention", "ruby"
+ context "when a valid language is specified" do
+ it "highlights as that language" do
+ result = filter('<pre lang="ruby"><code>def fun end</code></pre>')
+
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>')
end
- context "when an invalid language is specified" do
- it "highlights as plaintext" do
- result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
- else
- filter('<pre><code lang="gnuplot">This is a test</code></pre>')
- end
+ include_examples "XSS prevention", "ruby"
+ end
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
- end
+ context "when an invalid language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
- include_examples "XSS prevention", "gnuplot"
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
end
- context "languages that should be passed through" do
- let(:delimiter) { described_class::LANG_PARAMS_DELIMITER }
- let(:data_attr) { described_class::LANG_PARAMS_ATTR }
+ include_examples "XSS prevention", "gnuplot"
+ end
- %w(math mermaid plantuml suggestion).each do |lang|
- context "when #{lang} is specified" do
- it "highlights as plaintext but with the correct language attribute and class" do
- result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
- else
- filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
- end
+ context "languages that should be passed through" do
+ let(:delimiter) { described_class::LANG_PARAMS_DELIMITER }
+ let(:data_attr) { described_class::LANG_PARAMS_ATTR }
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
- end
+ %w(math mermaid plantuml suggestion).each do |lang|
+ context "when #{lang} is specified" do
+ it "highlights as plaintext but with the correct language attribute and class" do
+ result = filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
- include_examples "XSS prevention", lang
+ expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end
- context "when #{lang} has extra params" do
- let(:lang_params) { 'foo-bar-kux' }
-
- let(:xss_lang) do
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
- else
- "#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
- end
- end
-
- it "includes data-lang-params tag with extra information" do
- result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
- else
- filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
- end
-
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
- end
-
- include_examples "XSS prevention", lang
-
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- include_examples "XSS prevention",
- "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
- else
- include_examples "XSS prevention",
- "#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
- end
-
- include_examples "XSS prevention",
- "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>"
- end
+ include_examples "XSS prevention", lang
end
- context 'when multiple param delimiters are used' do
- let(:lang) { 'suggestion' }
- let(:lang_params) { '-1+10' }
+ context "when #{lang} has extra params" do
+ let(:lang_params) { 'foo-bar-kux' }
+ let(:xss_lang) { "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;" }
- let(:expected_result) do
- %{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
- end
-
- context 'when delimiter is space' do
- it 'delimits on the first appearance' do
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
+ it "includes data-lang-params tag with extra information" do
+ result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
- expect(result.to_html.delete("\n")).to eq(expected_result)
- else
- result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
-
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
- end
- end
+ expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end
- context 'when delimiter is colon' do
- it 'delimits on the first appearance' do
- result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
+ include_examples "XSS prevention", lang
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- expect(result.to_html.delete("\n")).to eq(expected_result)
- else
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">This is a test</span></code></pre><copy-code></copy-code></div>})
- end
- end
- end
+ include_examples "XSS prevention",
+ "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
+
+ include_examples "XSS prevention",
+ "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>"
end
end
- context "when sourcepos metadata is available" do
- it "includes it in the highlighted code block" do
- result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
+ context 'when multiple param delimiters are used' do
+ let(:lang) { 'suggestion' }
+ let(:lang_params) { '-1+10' }
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
+ let(:expected_result) do
+ %{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
end
- end
- context "when Rouge lexing fails" do
- before do
- allow_next_instance_of(Rouge::Lexers::Ruby) do |instance|
- allow(instance).to receive(:stream_tokens).and_raise(StandardError)
+ context 'when delimiter is space' do
+ it 'delimits on the first appearance' do
+ result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
+
+ expect(result.to_html.delete("\n")).to eq(expected_result)
end
end
- it "highlights as plaintext" do
- result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- filter('<pre lang="ruby"><code>This is a test</code></pre>')
- else
- filter('<pre><code lang="ruby">This is a test</code></pre>')
- end
+ context 'when delimiter is colon' do
+ it 'delimits on the first appearance' do
+ result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq(expected_result)
+ end
end
-
- include_examples "XSS prevention", "ruby"
end
+ end
- context "when Rouge lexing fails after a retry" do
- before do
- allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
- allow(instance).to receive(:stream_tokens).and_raise(StandardError)
- end
- end
+ context "when sourcepos metadata is available" do
+ it "includes it in the highlighted code block" do
+ result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
- it "does not add highlighting classes" do
- result = filter('<pre><code>This is a test</code></pre>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
+ end
+ end
- expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
+ context "when Rouge lexing fails" do
+ before do
+ allow_next_instance_of(Rouge::Lexers::Ruby) do |instance|
+ allow(instance).to receive(:stream_tokens).and_raise(StandardError)
end
+ end
+
+ it "highlights as plaintext" do
+ result = filter('<pre lang="ruby"><code>This is a test</code></pre>')
- include_examples "XSS prevention", "ruby"
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>')
end
+
+ include_examples "XSS prevention", "ruby"
end
- context 'using ruby-based HTML renderer' do
+ context "when Rouge lexing fails after a retry" do
before do
- stub_feature_flags(use_cmark_renderer: false)
+ allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
+ allow(instance).to receive(:stream_tokens).and_raise(StandardError)
+ end
end
- it_behaves_like 'renders correct markdown'
- end
+ it "does not add highlighting classes" do
+ result = filter('<pre><code>This is a test</code></pre>')
- context 'using c-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: true)
+ expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
end
- it_behaves_like 'renders correct markdown'
+ include_examples "XSS prevention", "ruby"
end
end
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 620b7d97a5b..376edfb99fc 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -65,47 +65,6 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote.strip
end
-
- context 'using ruby-based HTML renderer' do
- let(:html) { described_class.to_html(footnote_markdown, project: project) }
- let(:identifier) { html[/.*fnref1-(\d+).*/, 1] }
- let(:footnote_markdown) do
- <<~EOF
- first[^1] and second[^second] and twenty[^twenty]
- [^1]: one
- [^second]: two
- [^twenty]: twenty
- EOF
- end
-
- let(:filtered_footnote) do
- <<~EOF
- <p dir="auto">first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn3-#{identifier}" id="fnref3-#{identifier}">3</a></sup></p>
-
- <section class="footnotes"><ol>
- <li id="fn1-#{identifier}">
- <p>one <a href="#fnref1-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
- </li>
- <li id="fn2-#{identifier}">
- <p>two <a href="#fnref2-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
- </li>
- <li id="fn3-#{identifier}">
- <p>twenty <a href="#fnref3-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
- </li>
- </ol></section>
- EOF
- end
-
- before do
- stub_feature_flags(use_cmark_renderer: false)
- end
-
- it 'properly adds the necessary ids and classes' do
- stub_commonmark_sourcepos_disabled
-
- expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
- end
- end
end
describe 'links are detected as malicious' do
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
index c8cd9d4fcac..80392fe264f 100644
--- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -5,117 +5,93 @@ require 'spec_helper'
RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
using RSpec::Parameterized::TableSyntax
- shared_examples_for 'renders correct markdown' do
- describe 'CommonMark tests', :aggregate_failures do
- it 'converts all reference punctuation to literals' do
- reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
- markdown = reference_chars.split('').map {|char| char.prepend("\\") }.join
- punctuation = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.split('')
- punctuation = punctuation.delete_if {|char| char == '&' }
- punctuation << '&amp;'
-
- result = described_class.call(markdown, project: project)
- output = result[:output].to_html
-
- punctuation.each { |char| expect(output).to include("<span>#{char}</span>") }
- expect(result[:escaped_literals]).to be_truthy
- end
+ describe 'backslash escapes', :aggregate_failures do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
- it 'ensure we handle all the GitLab reference characters', :eager_load do
- reference_chars = ObjectSpace.each_object(Class).map do |klass|
- next unless klass.included_modules.include?(Referable)
- next unless klass.respond_to?(:reference_prefix)
- next unless klass.reference_prefix.length == 1
+ it 'converts all reference punctuation to literals' do
+ reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
+ markdown = reference_chars.split('').map {|char| char.prepend("\\") }.join
+ punctuation = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.split('')
+ punctuation = punctuation.delete_if {|char| char == '&' }
+ punctuation << '&amp;'
- klass.reference_prefix
- end.compact
+ result = described_class.call(markdown, project: project)
+ output = result[:output].to_html
- reference_chars.all? do |char|
- Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.include?(char)
- end
- end
+ punctuation.each { |char| expect(output).to include("<span>#{char}</span>") }
+ expect(result[:escaped_literals]).to be_truthy
+ end
- it 'does not convert non-reference punctuation to spans' do
- markdown = %q(\"\'\*\+\,\-\.\/\:\;\<\=\>\?\[\]\_\`\{\|\}) + %q[\(\)\\\\]
+ it 'ensure we handle all the GitLab reference characters', :eager_load do
+ reference_chars = ObjectSpace.each_object(Class).map do |klass|
+ next unless klass.included_modules.include?(Referable)
+ next unless klass.respond_to?(:reference_prefix)
+ next unless klass.reference_prefix.length == 1
- result = described_class.call(markdown, project: project)
- output = result[:output].to_html
+ klass.reference_prefix
+ end.compact
- expect(output).not_to include('<span>')
- expect(result[:escaped_literals]).to be_falsey
+ reference_chars.all? do |char|
+ Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS.include?(char)
end
+ end
- it 'does not convert other characters to literals' do
- markdown = %q(\→\A\a\ \3\φ\«)
- expected = '\→\A\a\ \3\φ\«'
-
- result = correct_html_included(markdown, expected)
- expect(result[:escaped_literals]).to be_falsey
- end
+ it 'does not convert non-reference punctuation to spans' do
+ markdown = %q(\"\'\*\+\,\-\.\/\:\;\<\=\>\?\[\]\_\`\{\|\}) + %q[\(\)\\\\]
- describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do
- where(:markdown, :expected) do
- %q(`` \@\! ``) | %q(<code>\@\!</code>)
- %q( \@\!) | %Q(<code>\\@\\!\n</code>)
- %Q(~~~\n\\@\\!\n~~~) | %Q(<code>\\@\\!\n</code>)
- %q(<http://example.com?find=\@>) | %q(<a href="http://example.com?find=%5C@">http://example.com?find=\@</a>)
- %q[<a href="/bar\@)">] | %q[<a href="/bar%5C@)">]
- end
-
- with_them do
- it { correct_html_included(markdown, expected) }
- end
- end
+ result = described_class.call(markdown, project: project)
+ output = result[:output].to_html
- describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
- let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
-
- it 'renders correct html' do
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>))
- else
- correct_html_included(markdown, %Q(<code lang="foo@bar">foo\n</code>))
- end
- end
-
- where(:markdown, :expected) do
- %q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
- %Q![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
- end
-
- with_them do
- it { correct_html_included(markdown, expected) }
- end
- end
+ expect(output).not_to include('<span>')
+ expect(result[:escaped_literals]).to be_falsey
end
- end
-
- describe 'backslash escapes' do
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:issue) { create(:issue, project: project) }
-
- def correct_html_included(markdown, expected)
- result = described_class.call(markdown, {})
- expect(result[:output].to_html).to include(expected)
+ it 'does not convert other characters to literals' do
+ markdown = %q(\→\A\a\ \3\φ\«)
+ expected = '\→\A\a\ \3\φ\«'
- result
+ result = correct_html_included(markdown, expected)
+ expect(result[:escaped_literals]).to be_falsey
end
- context 'using ruby-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: false)
+ describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do
+ where(:markdown, :expected) do
+ %q(`` \@\! ``) | %q(<code>\@\!</code>)
+ %q( \@\!) | %Q(<code>\\@\\!\n</code>)
+ %Q(~~~\n\\@\\!\n~~~) | %Q(<code>\\@\\!\n</code>)
+ %q(<http://example.com?find=\@>) | %q(<a href="http://example.com?find=%5C@">http://example.com?find=\@</a>)
+ %q[<a href="/bar\@)">] | %q[<a href="/bar%5C@)">]
end
- it_behaves_like 'renders correct markdown'
+ with_them do
+ it { correct_html_included(markdown, expected) }
+ end
end
- context 'using c-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: true)
+ describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
+ let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
+
+ it 'renders correct html' do
+ correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>))
+ end
+
+ where(:markdown, :expected) do
+ %q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
+ %Q![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
end
- it_behaves_like 'renders correct markdown'
+ with_them do
+ it { correct_html_included(markdown, expected) }
+ end
end
end
+
+ def correct_html_included(markdown, expected)
+ result = described_class.call(markdown, {})
+
+ expect(result[:output].to_html).to include(expected)
+
+ result
+ end
end
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index 04c35c8b082..3fbda7f3239 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -23,14 +23,6 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
end
it_behaves_like "referenced feature visibility", "merge_requests"
-
- context 'when optimize_merge_request_parser feature flag is off' do
- before do
- stub_feature_flags(optimize_merge_request_parser: false)
- end
-
- it_behaves_like "referenced feature visibility", "merge_requests"
- end
end
end
diff --git a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb
index bd306233de8..d6e19a5fc85 100644
--- a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb
+++ b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do
before do
allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
- subject.instance_variable_set(:@tmp_dir, tmpdir)
+ subject.instance_variable_set(:@tmpdir, tmpdir)
end
after(:all) do
@@ -43,11 +43,11 @@ RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do
end
end
- describe '#remove_tmp_dir' do
+ describe '#remove_tmpdir' do
it 'removes tmp dir' do
expect(FileUtils).to receive(:remove_entry).with(tmpdir).once
- subject.remove_tmp_dir
+ subject.remove_tmpdir
end
end
end
diff --git a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
index 3b5ea131d0d..9d43bb3ebfb 100644
--- a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
- let_it_be(:tmpdir) { Dir.mktmpdir }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
+ let(:tmpdir) { Dir.mktmpdir }
let(:uploads_dir_path) { File.join(tmpdir, '72a497a02fe3ee09edae2ed06d390038') }
let(:upload_file_path) { File.join(uploads_dir_path, 'upload.txt')}
let(:tracker) { create(:bulk_import_tracker, entity: entity) }
@@ -80,10 +80,10 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
.with(
configuration: context.configuration,
relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=uploads",
- dir: tmpdir,
+ tmpdir: tmpdir,
filename: 'uploads.tar.gz')
.and_return(download_service)
- expect(BulkImports::FileDecompressionService).to receive(:new).with(dir: tmpdir, filename: 'uploads.tar.gz').and_return(decompression_service)
+ expect(BulkImports::FileDecompressionService).to receive(:new).with(tmpdir: tmpdir, filename: 'uploads.tar.gz').and_return(decompression_service)
expect(BulkImports::ArchiveExtractionService).to receive(:new).with(tmpdir: tmpdir, filename: 'uploads.tar').and_return(extraction_service)
expect(download_service).to receive(:execute)
@@ -123,6 +123,31 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
end
end
end
+
+ describe '#after_run' do
+ before do
+ allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir)
+ end
+
+ it 'removes tmp dir' do
+ allow(FileUtils).to receive(:remove_entry).and_call_original
+ expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original
+
+ pipeline.after_run(nil)
+
+ expect(Dir.exist?(tmpdir)).to eq(false)
+ end
+
+ context 'when dir does not exist' do
+ it 'does not attempt to remove tmpdir' do
+ FileUtils.remove_entry(tmpdir)
+
+ expect(FileUtils).not_to receive(:remove_entry).with(tmpdir)
+
+ pipeline.after_run(nil)
+ end
+ end
+ end
end
context 'when importing to group' do
diff --git a/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb
index 11c475318bb..df7ff5b8062 100644
--- a/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do
subject(:pipeline) { described_class.new(context) }
before do
- allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
+ allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir)
end
after do
@@ -95,13 +95,13 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do
.with(
configuration: context.configuration,
relative_url: "/#{entity.pluralized_name}/#{entity.source_full_path}/export_relations/download?relation=self",
- dir: tmpdir,
+ tmpdir: tmpdir,
filename: 'self.json.gz')
.and_return(file_download_service)
expect(BulkImports::FileDecompressionService)
.to receive(:new)
- .with(dir: tmpdir, filename: 'self.json.gz')
+ .with(tmpdir: tmpdir, filename: 'self.json.gz')
.and_return(file_decompression_service)
expect(file_download_service).to receive(:execute)
@@ -156,4 +156,25 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline do
pipeline.json_attributes
end
end
+
+ describe '#after_run' do
+ it 'removes tmp dir' do
+ allow(FileUtils).to receive(:remove_entry).and_call_original
+ expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original
+
+ pipeline.after_run(nil)
+
+ expect(Dir.exist?(tmpdir)).to eq(false)
+ end
+
+ context 'when dir does not exist' do
+ it 'does not attempt to remove tmpdir' do
+ FileUtils.remove_entry(tmpdir)
+
+ expect(FileUtils).not_to receive(:remove_entry).with(tmpdir)
+
+ pipeline.after_run(nil)
+ end
+ end
+ end
end
diff --git a/spec/lib/error_tracking/collector/payload_validator_spec.rb b/spec/lib/error_tracking/collector/payload_validator_spec.rb
index ab5ec448dff..94708f63bf4 100644
--- a/spec/lib/error_tracking/collector/payload_validator_spec.rb
+++ b/spec/lib/error_tracking/collector/payload_validator_spec.rb
@@ -18,37 +18,25 @@ RSpec.describe ErrorTracking::Collector::PayloadValidator do
end
end
- context 'ruby payload' do
- let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) }
-
- it_behaves_like 'valid payload'
- end
-
- context 'python payload' do
- let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/python_event.json')) }
-
- it_behaves_like 'valid payload'
- end
-
- context 'python payload in repl' do
- let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/python_event_repl.json')) }
-
- it_behaves_like 'valid payload'
- end
+ context 'with event fixtures' do
+ where(:event_fixture) do
+ Dir.glob(Rails.root.join('spec/fixtures/error_tracking/*event*.json'))
+ end
- context 'browser payload' do
- let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/browser_event.json')) }
+ with_them do
+ let(:payload) { Gitlab::Json.parse(fixture_file(event_fixture)) }
- it_behaves_like 'valid payload'
+ it_behaves_like 'valid payload'
+ end
end
- context 'empty payload' do
+ context 'when empty' do
let(:payload) { '' }
it_behaves_like 'invalid payload'
end
- context 'invalid payload' do
+ context 'when invalid' do
let(:payload) { { 'foo' => 'bar' } }
it_behaves_like 'invalid payload'
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 82580d5d700..8c546390201 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -3,12 +3,40 @@
require 'spec_helper'
RSpec.describe Feature, stub_feature_flags: false do
+ include StubVersion
+
before do
# reset Flipper AR-engine
Feature.reset
skip_feature_flags_yaml_validation
end
+ describe '.feature_flags_available?' do
+ it 'returns false on connection error' do
+ expect(ActiveRecord::Base.connection).to receive(:active?).and_raise(PG::ConnectionBad) # rubocop:disable Database/MultipleDatabases
+
+ expect(described_class.feature_flags_available?).to eq(false)
+ end
+
+ it 'returns false when connection is not active' do
+ expect(ActiveRecord::Base.connection).to receive(:active?).and_return(false) # rubocop:disable Database/MultipleDatabases
+
+ expect(described_class.feature_flags_available?).to eq(false)
+ end
+
+ it 'returns false when the flipper table does not exist' do
+ expect(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
+
+ expect(described_class.feature_flags_available?).to eq(false)
+ end
+
+ it 'returns false on NoDatabaseError' do
+ expect(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError)
+
+ expect(described_class.feature_flags_available?).to eq(false)
+ end
+ end
+
describe '.get' do
let(:feature) { double(:feature) }
let(:key) { 'my_feature' }
@@ -585,6 +613,10 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when flag is new and not feature_flag_state_logs' do
let(:milestone) { "14.6" }
+ before do
+ stub_version('14.5.123', 'deadbeef')
+ end
+
it { is_expected.to be_truthy }
end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 7200ff3c4db..44bbbe49cd3 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -11,13 +11,27 @@ module Gitlab
allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
end
- shared_examples_for 'renders correct asciidoc' do
- context "without project" do
- let(:input) { '<b>ascii</b>' }
- let(:context) { {} }
- let(:html) { 'H<sub>2</sub>O' }
+ context "without project" do
+ let(:input) { '<b>ascii</b>' }
+ let(:context) { {} }
+ let(:html) { 'H<sub>2</sub>O' }
+
+ it "converts the input using Asciidoctor and default options" do
+ expected_asciidoc_opts = {
+ safe: :secure,
+ backend: :gitlab_html5,
+ attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
+ extensions: be_a(Proc)
+ }
+
+ expect(Asciidoctor).to receive(:convert)
+ .with(input, expected_asciidoc_opts).and_return(html)
+
+ expect(render(input, context)).to eq(html)
+ end
- it "converts the input using Asciidoctor and default options" do
+ context "with asciidoc_opts" do
+ it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
@@ -28,845 +42,808 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- expect(render(input, context)).to eq(html)
+ render(input, context)
end
+ end
- context "with asciidoc_opts" do
- it "merges the options with default ones" do
- expected_asciidoc_opts = {
- safe: :secure,
- backend: :gitlab_html5,
- attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
- extensions: be_a(Proc)
- }
+ context "with requested path" do
+ input = <<~ADOC
+ Document name: {docname}.
+ ADOC
+
+ it "ignores {docname} when not available" do
+ expect(render(input, {})).to include(input.strip)
+ end
+
+ [
+ ['/', '', 'root'],
+ ['README', 'README', 'just a filename'],
+ ['doc/api/', '', 'a directory'],
+ ['doc/api/README.adoc', 'README', 'a complete path']
+ ].each do |path, basename, desc|
+ it "sets {docname} for #{desc}" do
+ expect(render(input, { requested_path: path })).to include(": #{basename}.")
+ end
+ end
+ end
- expect(Asciidoctor).to receive(:convert)
- .with(input, expected_asciidoc_opts).and_return(html)
+ context "XSS" do
+ items = {
+ 'link with extra attribute' => {
+ input: 'link:mylink"onmouseover="alert(1)[Click Here]',
+ output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>"
+ },
+ 'link with unsafe scheme' => {
+ input: 'link:data://danger[Click Here]',
+ output: "<div>\n<p><a>Click Here</a></p>\n</div>"
+ },
+ 'image with onerror' => {
+ input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
+ output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ }
+ }
- render(input, context)
+ items.each do |name, data|
+ it "does not convert dangerous #{name} into HTML" do
+ expect(render(data[:input], context)).to include(data[:output])
end
end
- context "with requested path" do
+ # `stub_feature_flags method` runs AFTER declaration of `items` above.
+ # So the spec in its current implementation won't pass.
+ # Move this test back to the items hash when removing `use_cmark_renderer` feature flag.
+ it "does not convert dangerous fenced code with inline script into HTML" do
+ input = '```mypre"><script>alert(3)</script>'
+ output = "<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>"
+
+ expect(render(input, context)).to include(output)
+ end
+
+ it 'does not allow locked attributes to be overridden' do
input = <<~ADOC
- Document name: {docname}.
+ {counter:max-include-depth:1234}
+ <|-- {max-include-depth}
ADOC
- it "ignores {docname} when not available" do
- expect(render(input, {})).to include(input.strip)
- end
+ expect(render(input, {})).not_to include('1234')
+ end
+ end
- [
- ['/', '', 'root'],
- ['README', 'README', 'just a filename'],
- ['doc/api/', '', 'a directory'],
- ['doc/api/README.adoc', 'README', 'a complete path']
- ].each do |path, basename, desc|
- it "sets {docname} for #{desc}" do
- expect(render(input, { requested_path: path })).to include(": #{basename}.")
- end
- end
+ context "images" do
+ it "does lazy load and link image" do
+ input = 'image:https://localhost.com/image.png[]'
+ output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ expect(render(input, context)).to include(output)
end
- context "XSS" do
- items = {
- 'link with extra attribute' => {
- input: 'link:mylink"onmouseover="alert(1)[Click Here]',
- output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>"
- },
- 'link with unsafe scheme' => {
- input: 'link:data://danger[Click Here]',
- output: "<div>\n<p><a>Click Here</a></p>\n</div>"
- },
- 'image with onerror' => {
- input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
- }
- }
+ it "does not automatically link image if link is explicitly defined" do
+ input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
+ output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ expect(render(input, context)).to include(output)
+ end
+ end
- items.each do |name, data|
- it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:input], context)).to include(data[:output])
- end
- end
+ context 'with admonition' do
+ it 'preserves classes' do
+ input = <<~ADOC
+ NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
+ ADOC
- # `stub_feature_flags method` runs AFTER declaration of `items` above.
- # So the spec in its current implementation won't pass.
- # Move this test back to the items hash when removing `use_cmark_renderer` feature flag.
- it "does not convert dangerous fenced code with inline script into HTML" do
- input = '```mypre"><script>alert(3)</script>'
- output =
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- "<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>"
- else
- "<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>"
- end
+ output = <<~HTML
+ <div class="admonitionblock">
+ <table>
+ <tr>
+ <td class="icon">
+ <i class="fa icon-note" title="Note"></i>
+ </td>
+ <td>
+ An admonition paragraph, like this note, grabs the reader’s attention.
+ </td>
+ </tr>
+ </table>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
- expect(render(input, context)).to include(output)
- end
+ context 'with passthrough' do
+ it 'removes non heading ids' do
+ input = <<~ADOC
+ ++++
+ <h2 id="foo">Title</h2>
+ ++++
+ ADOC
- it 'does not allow locked attributes to be overridden' do
- input = <<~ADOC
- {counter:max-include-depth:1234}
- <|-- {max-include-depth}
- ADOC
+ output = <<~HTML
+ <h2>Title</h2>
+ HTML
- expect(render(input, {})).not_to include('1234')
- end
+ expect(render(input, context)).to include(output.strip)
end
- context "images" do
- it "does lazy load and link image" do
- input = 'image:https://localhost.com/image.png[]'
- output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
- expect(render(input, context)).to include(output)
- end
+ it 'removes non footnote def ids' do
+ input = <<~ADOC
+ ++++
+ <div id="def">Footnote definition</div>
+ ++++
+ ADOC
- it "does not automatically link image if link is explicitly defined" do
- input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
- output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
- expect(render(input, context)).to include(output)
- end
+ output = <<~HTML
+ <div>Footnote definition</div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
- context 'with admonition' do
- it 'preserves classes' do
- input = <<~ADOC
- NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
- ADOC
+ it 'removes non footnote ref ids' do
+ input = <<~ADOC
+ ++++
+ <a id="ref">Footnote reference</a>
+ ++++
+ ADOC
- output = <<~HTML
- <div class="admonitionblock">
- <table>
- <tr>
- <td class="icon">
- <i class="fa icon-note" title="Note"></i>
- </td>
- <td>
- An admonition paragraph, like this note, grabs the reader’s attention.
- </td>
- </tr>
- </table>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ output = <<~HTML
+ <a>Footnote reference</a>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with passthrough' do
- it 'removes non heading ids' do
- input = <<~ADOC
- ++++
- <h2 id="foo">Title</h2>
- ++++
- ADOC
+ context 'with footnotes' do
+ it 'preserves ids and links' do
+ input = <<~ADOC
+ This paragraph has a footnote.footnote:[This is the text of the footnote.]
+ ADOC
- output = <<~HTML
- <h2>Title</h2>
- HTML
+ output = <<~HTML
+ <div>
+ <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
+ </div>
+ <div>
+ <hr>
+ <div id="_footnotedef_1">
+ <a href="#_footnoteref_1">1</a>. This is the text of the footnote.
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
- expect(render(input, context)).to include(output.strip)
- end
+ context 'with section anchors' do
+ it 'preserves ids and links' do
+ input = <<~ADOC
+ = Title
- it 'removes non footnote def ids' do
- input = <<~ADOC
- ++++
- <div id="def">Footnote definition</div>
- ++++
- ADOC
+ == First section
- output = <<~HTML
- <div>Footnote definition</div>
- HTML
+ This is the first section.
- expect(render(input, context)).to include(output.strip)
- end
+ == Second section
- it 'removes non footnote ref ids' do
- input = <<~ADOC
- ++++
- <a id="ref">Footnote reference</a>
- ++++
- ADOC
+ This is the second section.
- output = <<~HTML
- <a>Footnote reference</a>
- HTML
+ == Thunder âš¡ !
- expect(render(input, context)).to include(output.strip)
- end
+ This is the third section.
+ ADOC
+
+ output = <<~HTML
+ <h1>Title</h1>
+ <div>
+ <h2 id="user-content-first-section">
+ <a class="anchor" href="#user-content-first-section"></a>First section</h2>
+ <div>
+ <div>
+ <p>This is the first section.</p>
+ </div>
+ </div>
+ </div>
+ <div>
+ <h2 id="user-content-second-section">
+ <a class="anchor" href="#user-content-second-section"></a>Second section</h2>
+ <div>
+ <div>
+ <p>This is the second section.</p>
+ </div>
+ </div>
+ </div>
+ <div>
+ <h2 id="user-content-thunder">
+ <a class="anchor" href="#user-content-thunder"></a>Thunder âš¡ !</h2>
+ <div>
+ <div>
+ <p>This is the third section.</p>
+ </div>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with footnotes' do
- it 'preserves ids and links' do
- input = <<~ADOC
- This paragraph has a footnote.footnote:[This is the text of the footnote.]
- ADOC
+ context 'with xrefs' do
+ it 'preserves ids' do
+ input = <<~ADOC
+ Learn how to xref:cross-references[use cross references].
- output = <<~HTML
- <div>
- <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
- </div>
- <div>
- <hr>
- <div id="_footnotedef_1">
- <a href="#_footnoteref_1">1</a>. This is the text of the footnote.
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <p>Learn how to <a href="#cross-references">use cross references</a>.</p>
+ </div>
+ <div>
+ <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with section anchors' do
- it 'preserves ids and links' do
- input = <<~ADOC
- = Title
-
- == First section
-
- This is the first section.
-
- == Second section
-
- This is the second section.
-
- == Thunder âš¡ !
-
- This is the third section.
- ADOC
+ context 'with checklist' do
+ it 'preserves classes' do
+ input = <<~ADOC
+ * [x] checked
+ * [ ] not checked
+ ADOC
- output = <<~HTML
- <h1>Title</h1>
- <div>
- <h2 id="user-content-first-section">
- <a class="anchor" href="#user-content-first-section"></a>First section</h2>
- <div>
- <div>
- <p>This is the first section.</p>
- </div>
- </div>
- </div>
- <div>
- <h2 id="user-content-second-section">
- <a class="anchor" href="#user-content-second-section"></a>Second section</h2>
- <div>
- <div>
- <p>This is the second section.</p>
- </div>
- </div>
- </div>
- <div>
- <h2 id="user-content-thunder">
- <a class="anchor" href="#user-content-thunder"></a>Thunder âš¡ !</h2>
- <div>
- <div>
- <p>This is the third section.</p>
- </div>
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ output = <<~HTML
+ <div>
+ <ul class="checklist">
+ <li>
+ <p><i class="fa fa-check-square-o"></i> checked</p>
+ </li>
+ <li>
+ <p><i class="fa fa-square-o"></i> not checked</p>
+ </li>
+ </ul>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with xrefs' do
- it 'preserves ids' do
- input = <<~ADOC
- Learn how to xref:cross-references[use cross references].
-
- [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
- ADOC
+ context 'with marks' do
+ it 'preserves classes' do
+ input = <<~ADOC
+ Werewolves are allergic to #cassia cinnamon#.
- output = <<~HTML
- <div>
- <p>Learn how to <a href="#cross-references">use cross references</a>.</p>
- </div>
- <div>
- <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
- </div>
- HTML
+ Did the werewolves read the [.small]#small print#?
- expect(render(input, context)).to include(output.strip)
- end
+ Where did all the [.underline.small]#cores# run off to?
+
+ We need [.line-through]#ten# make that twenty VMs.
+
+ [.big]##O##nce upon an infinite loop.
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <p>Werewolves are allergic to <mark>cassia cinnamon</mark>.</p>
+ </div>
+ <div>
+ <p>Did the werewolves read the <span class="small">small print</span>?</p>
+ </div>
+ <div>
+ <p>Where did all the <span class="underline small">cores</span> run off to?</p>
+ </div>
+ <div>
+ <p>We need <span class="line-through">ten</span> make that twenty VMs.</p>
+ </div>
+ <div>
+ <p><span class="big">O</span>nce upon an infinite loop.</p>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with checklist' do
- it 'preserves classes' do
- input = <<~ADOC
- * [x] checked
- * [ ] not checked
- ADOC
+ context 'with fenced block' do
+ it 'highlights syntax' do
+ input = <<~ADOC
+ ```js
+ console.log('hello world')
+ ```
+ ADOC
- output = <<~HTML
- <div>
- <ul class="checklist">
- <li>
- <p><i class="fa fa-check-square-o"></i> checked</p>
- </li>
- <li>
- <p><i class="fa fa-square-o"></i> not checked</p>
- </li>
- </ul>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ output = <<~HTML
+ <div>
+ <div>
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with marks' do
- it 'preserves classes' do
- input = <<~ADOC
- Werewolves are allergic to #cassia cinnamon#.
-
- Did the werewolves read the [.small]#small print#?
-
- Where did all the [.underline.small]#cores# run off to?
-
- We need [.line-through]#ten# make that twenty VMs.
-
- [.big]##O##nce upon an infinite loop.
- ADOC
+ context 'with listing block' do
+ it 'highlights syntax' do
+ input = <<~ADOC
+ [source,c++]
+ .class.cpp
+ ----
+ #include <stdio.h>
- output = <<~HTML
- <div>
- <p>Werewolves are allergic to <mark>cassia cinnamon</mark>.</p>
- </div>
- <div>
- <p>Did the werewolves read the <span class="small">small print</span>?</p>
- </div>
- <div>
- <p>Where did all the <span class="underline small">cores</span> run off to?</p>
- </div>
- <div>
- <p>We need <span class="line-through">ten</span> make that twenty VMs.</p>
- </div>
- <div>
- <p><span class="big">O</span>nce upon an infinite loop.</p>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ for (int i = 0; i < 5; i++) {
+ std::cout<<"*"<<std::endl;
+ }
+ ----
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>class.cpp</div>
+ <div>
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
+ <span id="LC2" class="line" lang="cpp"></span>
+ <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
+ <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
+ <span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with fenced block' do
- it 'highlights syntax' do
- input = <<~ADOC
- ```js
- console.log('hello world')
- ```
- ADOC
+ context 'with stem block' do
+ it 'does not apply syntax highlighting' do
+ input = <<~ADOC
+ [stem]
+ ++++
+ \sqrt{4} = 2
+ ++++
+ ADOC
- output = <<~HTML
- <div>
- <div>
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
- <copy-code></copy-code>
- </div>
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
+
+ expect(render(input, context)).to include(output)
end
+ end
- context 'with listing block' do
- it 'highlights syntax' do
- input = <<~ADOC
- [source,c++]
- .class.cpp
- ----
- #include <stdio.h>
-
- for (int i = 0; i < 5; i++) {
- std::cout<<"*"<<std::endl;
- }
- ----
- ADOC
+ context 'external links' do
+ it 'adds the `rel` attribute to the link' do
+ output = render('link:https://google.com[Google]', context)
- output = <<~HTML
- <div>
- <div>class.cpp</div>
- <div>
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
- <span id="LC2" class="line" lang="cpp"></span>
- <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
- <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
- <span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
- <copy-code></copy-code>
- </div>
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ expect(output).to include('rel="nofollow noreferrer noopener"')
end
+ end
- context 'with stem block' do
- it 'does not apply syntax highlighting' do
- input = <<~ADOC
- [stem]
- ++++
- \sqrt{4} = 2
- ++++
- ADOC
+ context 'LaTex code' do
+ it 'adds class js-render-math to the output' do
+ input = <<~MD
+ :stem: latexmath
- output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
+ [stem]
+ ++++
+ \sqrt{4} = 2
+ ++++
- expect(render(input, context)).to include(output)
- end
+ another part
+
+ [latexmath]
+ ++++
+ \beta_x \gamma
+ ++++
+
+ stem:[2+2] is 4
+ MD
+
+ expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
+ expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
end
+ end
- context 'external links' do
- it 'adds the `rel` attribute to the link' do
- output = render('link:https://google.com[Google]', context)
+ context 'outfilesuffix' do
+ it 'defaults to adoc' do
+ output = render("Inter-document reference <<README.adoc#>>", context)
- expect(output).to include('rel="nofollow noreferrer noopener"')
- end
+ expect(output).to include("a href=\"README.adoc\"")
end
+ end
- context 'LaTex code' do
- it 'adds class js-render-math to the output' do
- input = <<~MD
- :stem: latexmath
-
- [stem]
- ++++
- \sqrt{4} = 2
- ++++
-
- another part
-
- [latexmath]
- ++++
- \beta_x \gamma
- ++++
-
- stem:[2+2] is 4
- MD
-
- expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
- expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
- end
+ context 'with mermaid diagrams' do
+ it 'adds class js-render-mermaid to the output' do
+ input = <<~MD
+ [mermaid]
+ ....
+ graph LR
+ A[Square Rect] -- Link text --> B((Circle))
+ A --> C(Round Rect)
+ B --> D{Rhombus}
+ C --> D
+ ....
+ MD
+
+ output = <<~HTML
+ <pre data-mermaid-style="display" class="js-render-mermaid">graph LR
+ A[Square Rect] -- Link text --&gt; B((Circle))
+ A --&gt; C(Round Rect)
+ B --&gt; D{Rhombus}
+ C --&gt; D</pre>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
end
- context 'outfilesuffix' do
- it 'defaults to adoc' do
- output = render("Inter-document reference <<README.adoc#>>", context)
+ it 'applies subs in diagram block' do
+ input = <<~MD
+ :class-name: AveryLongClass
- expect(output).to include("a href=\"README.adoc\"")
- end
- end
+ [mermaid,subs=+attributes]
+ ....
+ classDiagram
+ Class01 <|-- {class-name} : Cool
+ ....
+ MD
- context 'with mermaid diagrams' do
- it 'adds class js-render-mermaid to the output' do
- input = <<~MD
- [mermaid]
- ....
- graph LR
- A[Square Rect] -- Link text --> B((Circle))
- A --> C(Round Rect)
- B --> D{Rhombus}
- C --> D
- ....
- MD
-
- output = <<~HTML
- <pre data-mermaid-style="display" class="js-render-mermaid">graph LR
- A[Square Rect] -- Link text --&gt; B((Circle))
- A --&gt; C(Round Rect)
- B --&gt; D{Rhombus}
- C --&gt; D</pre>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ output = <<~HTML
+ <pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
+ Class01 &lt;|-- AveryLongClass : Cool</pre>
+ HTML
- it 'applies subs in diagram block' do
- input = <<~MD
- :class-name: AveryLongClass
-
- [mermaid,subs=+attributes]
- ....
- classDiagram
- Class01 <|-- {class-name} : Cool
- ....
- MD
-
- output = <<~HTML
- <pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
- Class01 &lt;|-- AveryLongClass : Cool</pre>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ expect(render(input, context)).to include(output.strip)
end
+ end
- context 'with Kroki enabled' do
- before do
- allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
- allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
- end
-
- it 'converts a graphviz diagram to image' do
- input = <<~ADOC
- [graphviz]
- ....
- digraph G {
- Hello->World
- }
- ....
- ADOC
+ context 'with Kroki enabled' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
+ end
- output = <<~HTML
- <div>
- <div>
- <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
- </div>
- </div>
- HTML
+ it 'converts a graphviz diagram to image' do
+ input = <<~ADOC
+ [graphviz]
+ ....
+ digraph G {
+ Hello->World
+ }
+ ....
+ ADOC
- expect(render(input, context)).to include(output.strip)
- end
+ output = <<~HTML
+ <div>
+ <div>
+ <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
+ </div>
+ </div>
+ HTML
- it 'does not convert a blockdiag diagram to image' do
- input = <<~ADOC
- [blockdiag]
- ....
- blockdiag {
- Kroki -> generates -> "Block diagrams";
- Kroki -> is -> "very easy!";
-
- Kroki [color = "greenyellow"];
- "Block diagrams" [color = "pink"];
- "very easy!" [color = "orange"];
- }
- ....
- ADOC
+ expect(render(input, context)).to include(output.strip)
+ end
- output = <<~HTML
- <div>
- <div>
- <pre>blockdiag {
- Kroki -&gt; generates -&gt; "Block diagrams";
- Kroki -&gt; is -&gt; "very easy!";
-
- Kroki [color = "greenyellow"];
- "Block diagrams" [color = "pink"];
- "very easy!" [color = "orange"];
- }</pre>
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
+ it 'does not convert a blockdiag diagram to image' do
+ input = <<~ADOC
+ [blockdiag]
+ ....
+ blockdiag {
+ Kroki -> generates -> "Block diagrams";
+ Kroki -> is -> "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }
+ ....
+ ADOC
- it 'does not allow kroki-plantuml-include to be overridden' do
- input = <<~ADOC
- [plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
- ....
- class BlockProcessor
-
- BlockProcessor <|-- {counter:kroki-plantuml-include}
- ....
- ADOC
+ output = <<~HTML
+ <div>
+ <div>
+ <pre>blockdiag {
+ Kroki -&gt; generates -&gt; "Block diagrams";
+ Kroki -&gt; is -&gt; "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }</pre>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
- output = <<~HTML
- <div>
- <div>
- <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
- </div>
- </div>
- HTML
+ it 'does not allow kroki-plantuml-include to be overridden' do
+ input = <<~ADOC
+ [plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
+ ....
+ class BlockProcessor
- expect(render(input, {})).to include(output.strip)
- end
+ BlockProcessor <|-- {counter:kroki-plantuml-include}
+ ....
+ ADOC
- it 'does not allow kroki-server-url to be overridden' do
- input = <<~ADOC
- [plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
- ....
- class BlockProcessor
-
- BlockProcessor
- ....
- ADOC
+ output = <<~HTML
+ <div>
+ <div>
+ <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
+ </div>
+ </div>
+ HTML
- expect(render(input, {})).not_to include('evilsite')
- end
+ expect(render(input, {})).to include(output.strip)
end
- context 'with Kroki and BlockDiag (additional format) enabled' do
- before do
- allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
- allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
- allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true)
- end
-
- it 'converts a blockdiag diagram to image' do
- input = <<~ADOC
- [blockdiag]
- ....
- blockdiag {
- Kroki -> generates -> "Block diagrams";
- Kroki -> is -> "very easy!";
-
- Kroki [color = "greenyellow"];
- "Block diagrams" [color = "pink"];
- "very easy!" [color = "orange"];
- }
- ....
- ADOC
+ it 'does not allow kroki-server-url to be overridden' do
+ input = <<~ADOC
+ [plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
+ ....
+ class BlockProcessor
- output = <<~HTML
- <div>
- <div>
- <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
- </div>
- </div>
- HTML
+ BlockProcessor
+ ....
+ ADOC
- expect(render(input, context)).to include(output.strip)
- end
+ expect(render(input, {})).not_to include('evilsite')
end
end
- context 'with project' do
- let(:context) do
- {
- commit: commit,
- project: project,
- ref: ref,
- requested_path: requested_path
- }
+ context 'with Kroki and BlockDiag (additional format) enabled' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true)
end
- let(:commit) { project.commit(ref) }
- let(:project) { create(:project, :repository) }
- let(:ref) { 'asciidoc' }
- let(:requested_path) { '/' }
+ it 'converts a blockdiag diagram to image' do
+ input = <<~ADOC
+ [blockdiag]
+ ....
+ blockdiag {
+ Kroki -> generates -> "Block diagrams";
+ Kroki -> is -> "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }
+ ....
+ ADOC
- context 'include directive' do
- subject(:output) { render(input, context) }
+ output = <<~HTML
+ <div>
+ <div>
+ <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
+ </div>
+ </div>
+ HTML
- let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
+ end
- before do
- current_file = requested_path
- current_file += 'README.adoc' if requested_path.end_with? '/'
+ context 'with project' do
+ let(:context) do
+ {
+ commit: commit,
+ project: project,
+ ref: ref,
+ requested_path: requested_path
+ }
+ end
- create_file(current_file, "= AsciiDoc\n")
- end
+ let(:commit) { project.commit(ref) }
+ let(:project) { create(:project, :repository) }
+ let(:ref) { 'asciidoc' }
+ let(:requested_path) { '/' }
- def many_includes(target)
- Array.new(10, "include::#{target}[]").join("\n")
- end
+ context 'include directive' do
+ subject(:output) { render(input, context) }
- context 'cyclic imports' do
- before do
- create_file('doc/api/a.adoc', many_includes('b.adoc'))
- create_file('doc/api/b.adoc', many_includes('a.adoc'))
- end
+ let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
- let(:include_path) { 'a.adoc' }
- let(:requested_path) { 'doc/api/README.md' }
+ before do
+ current_file = requested_path
+ current_file += 'README.adoc' if requested_path.end_with? '/'
- it 'completes successfully' do
- is_expected.to include('<p>Include this:</p>')
- end
+ create_file(current_file, "= AsciiDoc\n")
+ end
+
+ def many_includes(target)
+ Array.new(10, "include::#{target}[]").join("\n")
+ end
+
+ context 'cyclic imports' do
+ before do
+ create_file('doc/api/a.adoc', many_includes('b.adoc'))
+ create_file('doc/api/b.adoc', many_includes('a.adoc'))
end
- context 'with path to non-existing file' do
- let(:include_path) { 'not-exists.adoc' }
+ let(:include_path) { 'a.adoc' }
+ let(:requested_path) { 'doc/api/README.md' }
- it 'renders Unresolved directive placeholder' do
- is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
- end
+ it 'completes successfully' do
+ is_expected.to include('<p>Include this:</p>')
end
+ end
- shared_examples :invalid_include do
- let(:include_path) { 'dk.png' }
+ context 'with path to non-existing file' do
+ let(:include_path) { 'not-exists.adoc' }
- before do
- allow(project.repository).to receive(:blob_at).and_return(blob)
- end
+ it 'renders Unresolved directive placeholder' do
+ is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
+ end
+ end
- it 'does not read the blob' do
- expect(blob).not_to receive(:data)
- end
+ shared_examples :invalid_include do
+ let(:include_path) { 'dk.png' }
- it 'renders Unresolved directive placeholder' do
- is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
- end
+ before do
+ allow(project.repository).to receive(:blob_at).and_return(blob)
end
- context 'with path to a binary file' do
- let(:blob) { fake_blob(path: 'dk.png', binary: true) }
+ it 'does not read the blob' do
+ expect(blob).not_to receive(:data)
+ end
- include_examples :invalid_include
+ it 'renders Unresolved directive placeholder' do
+ is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
end
+ end
- context 'with path to file in external storage' do
- let(:blob) { fake_blob(path: 'dk.png', lfs: true) }
+ context 'with path to a binary file' do
+ let(:blob) { fake_blob(path: 'dk.png', binary: true) }
- before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- project.update_attribute(:lfs_enabled, true)
- end
+ include_examples :invalid_include
+ end
- include_examples :invalid_include
+ context 'with path to file in external storage' do
+ let(:blob) { fake_blob(path: 'dk.png', lfs: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
end
- context 'with path to a textual file' do
- let(:include_path) { 'sample.adoc' }
+ include_examples :invalid_include
+ end
- before do
- create_file(file_path, "Content from #{include_path}")
- end
+ context 'with path to a textual file' do
+ let(:include_path) { 'sample.adoc' }
- shared_examples :valid_include do
- [
- ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
- ['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
- ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
- ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'],
- ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories']
- ].each do |include_path_, file_path_, desc|
- context "the file is specified by #{desc}" do
- let(:include_path) { include_path_ }
- let(:file_path) { file_path_ }
-
- it 'includes content of the file' do
- is_expected.to include('<p>Include this:</p>')
- is_expected.to include("<p>Content from #{include_path}</p>")
- end
+ before do
+ create_file(file_path, "Content from #{include_path}")
+ end
+
+ shared_examples :valid_include do
+ [
+ ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
+ ['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
+ ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
+ ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'],
+ ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories']
+ ].each do |include_path_, file_path_, desc|
+ context "the file is specified by #{desc}" do
+ let(:include_path) { include_path_ }
+ let(:file_path) { file_path_ }
+
+ it 'includes content of the file' do
+ is_expected.to include('<p>Include this:</p>')
+ is_expected.to include("<p>Content from #{include_path}</p>")
end
end
end
+ end
- context 'when requested path is a file in the repo' do
- let(:requested_path) { 'doc/api/README.adoc' }
+ context 'when requested path is a file in the repo' do
+ let(:requested_path) { 'doc/api/README.adoc' }
- include_examples :valid_include
+ include_examples :valid_include
- context 'without a commit (only ref)' do
- let(:commit) { nil }
+ context 'without a commit (only ref)' do
+ let(:commit) { nil }
- include_examples :valid_include
- end
+ include_examples :valid_include
end
+ end
- context 'when requested path is a directory in the repo' do
- let(:requested_path) { 'doc/api/' }
+ context 'when requested path is a directory in the repo' do
+ let(:requested_path) { 'doc/api/' }
- include_examples :valid_include
+ include_examples :valid_include
- context 'without a commit (only ref)' do
- let(:commit) { nil }
+ context 'without a commit (only ref)' do
+ let(:commit) { nil }
- include_examples :valid_include
- end
+ include_examples :valid_include
end
end
+ end
- context 'when repository is passed into the context' do
- let(:wiki_repo) { project.wiki.repository }
- let(:include_path) { 'wiki_file.adoc' }
+ context 'when repository is passed into the context' do
+ let(:wiki_repo) { project.wiki.repository }
+ let(:include_path) { 'wiki_file.adoc' }
+ before do
+ project.create_wiki
+ context.merge!(repository: wiki_repo)
+ end
+
+ context 'when the file exists' do
before do
- project.create_wiki
- context.merge!(repository: wiki_repo)
+ create_file(include_path, 'Content from wiki', repository: wiki_repo)
end
- context 'when the file exists' do
- before do
- create_file(include_path, 'Content from wiki', repository: wiki_repo)
- end
+ it { is_expected.to include('<p>Content from wiki</p>') }
+ end
- it { is_expected.to include('<p>Content from wiki</p>') }
- end
+ context 'when the file does not exist' do
+ it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
+ end
+ end
- context 'when the file does not exist' do
- it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
- end
+ context 'recursive includes with relative paths' do
+ let(:input) do
+ <<~ADOC
+ Source: requested file
+
+ include::doc/README.adoc[]
+
+ include::license.adoc[]
+ ADOC
end
- context 'recursive includes with relative paths' do
- let(:input) do
- <<~ADOC
- Source: requested file
-
- include::doc/README.adoc[]
-
- include::license.adoc[]
- ADOC
- end
+ before do
+ create_file 'doc/README.adoc', <<~ADOC
+ Source: doc/README.adoc
- before do
- create_file 'doc/README.adoc', <<~ADOC
- Source: doc/README.adoc
-
- include::../license.adoc[]
-
- include::api/hello.adoc[]
- ADOC
- create_file 'license.adoc', <<~ADOC
- Source: license.adoc
- ADOC
- create_file 'doc/api/hello.adoc', <<~ADOC
- Source: doc/api/hello.adoc
-
- include::./common.adoc[]
- ADOC
- create_file 'doc/api/common.adoc', <<~ADOC
- Source: doc/api/common.adoc
- ADOC
- end
+ include::../license.adoc[]
- it 'includes content of the included files recursively' do
- expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
- Source: requested file
- Source: doc/README.adoc
- Source: license.adoc
- Source: doc/api/hello.adoc
- Source: doc/api/common.adoc
- Source: license.adoc
- ADOC
- end
+ include::api/hello.adoc[]
+ ADOC
+ create_file 'license.adoc', <<~ADOC
+ Source: license.adoc
+ ADOC
+ create_file 'doc/api/hello.adoc', <<~ADOC
+ Source: doc/api/hello.adoc
+
+ include::./common.adoc[]
+ ADOC
+ create_file 'doc/api/common.adoc', <<~ADOC
+ Source: doc/api/common.adoc
+ ADOC
end
- def create_file(path, content, repository: project.repository)
- repository.create_file(project.creator, path, content,
- message: "Add #{path}", branch_name: 'asciidoc')
+ it 'includes content of the included files recursively' do
+ expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
+ Source: requested file
+ Source: doc/README.adoc
+ Source: license.adoc
+ Source: doc/api/hello.adoc
+ Source: doc/api/common.adoc
+ Source: license.adoc
+ ADOC
end
end
- end
- end
- context 'using ruby-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: false)
- end
-
- it_behaves_like 'renders correct asciidoc'
- end
-
- context 'using c-based HTML renderer' do
- before do
- stub_feature_flags(use_cmark_renderer: true)
+ def create_file(path, content, repository: project.repository)
+ repository.create_file(project.creator, path, content,
+ message: "Add #{path}", branch_name: 'asciidoc')
+ end
end
-
- it_behaves_like 'renders correct asciidoc'
end
def render(*args)
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index f1c891b2adb..e985f66bfe9 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -939,21 +939,19 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#cluster_agent_token_from_authorization_token' do
- let_it_be(:agent_token, freeze: true) { create(:cluster_agent_token) }
+ let_it_be(:agent_token) { create(:cluster_agent_token) }
+
+ subject { cluster_agent_token_from_authorization_token }
context 'when route_setting is empty' do
- it 'returns nil' do
- expect(cluster_agent_token_from_authorization_token).to be_nil
- end
+ it { is_expected.to be_nil }
end
context 'when route_setting allows cluster agent token' do
let(:route_authentication_setting) { { cluster_agent_token_allowed: true } }
context 'Authorization header is empty' do
- it 'returns nil' do
- expect(cluster_agent_token_from_authorization_token).to be_nil
- end
+ it { is_expected.to be_nil }
end
context 'Authorization header is incorrect' do
@@ -961,9 +959,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
request.headers['Authorization'] = 'Bearer ABCD'
end
- it 'returns nil' do
- expect(cluster_agent_token_from_authorization_token).to be_nil
- end
+ it { is_expected.to be_nil }
end
context 'Authorization header is malformed' do
@@ -971,9 +967,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
request.headers['Authorization'] = 'Bearer'
end
- it 'returns nil' do
- expect(cluster_agent_token_from_authorization_token).to be_nil
- end
+ it { is_expected.to be_nil }
end
context 'Authorization header matches agent token' do
@@ -981,8 +975,14 @@ RSpec.describe Gitlab::Auth::AuthFinders do
request.headers['Authorization'] = "Bearer #{agent_token.token}"
end
- it 'returns the agent token' do
- expect(cluster_agent_token_from_authorization_token).to eq(agent_token)
+ it { is_expected.to eq(agent_token) }
+
+ context 'agent token has been revoked' do
+ before do
+ agent_token.revoked!
+ end
+
+ it { is_expected.to be_nil }
end
end
end
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 7a657cce597..3039fce6141 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -121,10 +121,40 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
expect(config.adapter_options).to eq(
host: 'ldap.example.com',
port: 386,
+ hosts: nil,
encryption: nil
)
end
+ it 'includes failover hosts when set' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'hosts' => [
+ ['ldap1.example.com', 636],
+ ['ldap2.example.com', 636]
+ ],
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.adapter_options).to include({
+ hosts: [
+ ['ldap1.example.com', 636],
+ ['ldap2.example.com', 636]
+ ],
+ auth: {
+ method: :simple,
+ username: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ }
+ })
+ end
+
it 'includes authentication options when auth is configured' do
stub_ldap_config(
options: {
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 32e647688ff..611c70d73a1 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
context 'when IP is already banned' do
- subject { gl_auth.find_for_git_client('username', 'password', project: nil, ip: 'ip') }
+ subject { gl_auth.find_for_git_client('username', Gitlab::Password.test_default, project: nil, ip: 'ip') }
before do
expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter|
@@ -204,16 +204,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
it 'recognizes master passwords' do
- user = create(:user, password: 'password')
+ user = create(:user, password: Gitlab::Password.test_default)
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
+ expect(gl_auth.find_for_git_client(user.username, Gitlab::Password.test_default, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
include_examples 'user login operation with unique ip limit' do
- let(:user) { create(:user, password: 'password') }
+ let(:user) { create(:user, password: Gitlab::Password.test_default) }
def operation
- expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
+ expect(gl_auth.find_for_git_client(user.username, Gitlab::Password.test_default, project: nil, ip: 'ip')).to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities)
end
end
@@ -477,7 +477,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
:user,
:blocked,
username: 'normal_user',
- password: 'my-secret'
+ password: Gitlab::Password.test_default
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
@@ -486,7 +486,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
context 'when 2fa is enabled globally' do
let_it_be(:user) do
- create(:user, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
+ create(:user, username: 'normal_user', password: Gitlab::Password.test_default, otp_grace_period_started_at: 1.day.ago)
end
before do
@@ -510,7 +510,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
context 'when 2fa is enabled personally' do
let(:user) do
- create(:user, :two_factor, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago)
+ create(:user, :two_factor, username: 'normal_user', password: Gitlab::Password.test_default, otp_grace_period_started_at: 1.day.ago)
end
it 'fails' do
@@ -523,7 +523,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
user = create(
:user,
username: 'normal_user',
- password: 'my-secret'
+ password: Gitlab::Password.test_default
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
@@ -534,7 +534,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
user = create(
:user,
username: 'oauth2',
- password: 'my-secret'
+ password: Gitlab::Password.test_default
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
@@ -609,7 +609,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
context 'when deploy token and user have the same username' do
let(:username) { 'normal_user' }
- let(:user) { create(:user, username: username, password: 'my-secret') }
+ let(:user) { create(:user, username: username, password: Gitlab::Password.test_default) }
let(:deploy_token) { create(:deploy_token, username: username, read_registry: false, projects: [project]) }
it 'succeeds for the token' do
@@ -622,7 +622,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'succeeds for the user' do
auth_success = { actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities }
- expect(gl_auth.find_for_git_client(username, 'my-secret', project: project, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(username, Gitlab::Password.test_default, project: project, ip: 'ip'))
.to have_attributes(auth_success)
end
end
@@ -816,7 +816,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
let(:username) { 'John' } # username isn't lowercase, test this
- let(:password) { 'my-secret' }
+ let(:password) { Gitlab::Password.test_default }
it "finds user by valid login/password" do
expect(gl_auth.find_with_user_password(username, password)).to eql user
@@ -941,13 +941,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it "does not find user by using ldap as fallback to for authentication" do
expect(Gitlab::Auth::Ldap::Authentication).to receive(:login).and_return(nil)
- expect(gl_auth.find_with_user_password('ldap_user', 'password')).to be_nil
+ expect(gl_auth.find_with_user_password('ldap_user', Gitlab::Password.test_default)).to be_nil
end
it "find new user by using ldap as fallback to for authentication" do
expect(Gitlab::Auth::Ldap::Authentication).to receive(:login).and_return(user)
- expect(gl_auth.find_with_user_password('ldap_user', 'password')).to eq(user)
+ expect(gl_auth.find_with_user_password('ldap_user', Gitlab::Password.test_default)).to eq(user)
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
index 6ab1e3ecd70..f5d2224747a 100644
--- a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20210301200959 do
subject(:perform) { migration.perform(1, 99) }
let(:migration) { described_class.new }
diff --git a/spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb
new file mode 100644
index 00000000000..8980a26932b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_ci_namespace_mirrors_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillCiNamespaceMirrors, :migration, schema: 20211208122200 do
+ let(:namespaces) { table(:namespaces) }
+ let(:ci_namespace_mirrors) { table(:ci_namespace_mirrors) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'creates hierarchies for all namespaces in range' do
+ namespaces.create!(id: 5, name: 'test1', path: 'test1')
+ namespaces.create!(id: 7, name: 'test2', path: 'test2')
+ namespaces.create!(id: 8, name: 'test3', path: 'test3')
+
+ subject.perform(5, 7)
+
+ expect(ci_namespace_mirrors.all).to contain_exactly(
+ an_object_having_attributes(namespace_id: 5, traversal_ids: [5]),
+ an_object_having_attributes(namespace_id: 7, traversal_ids: [7])
+ )
+ end
+
+ it 'handles existing hierarchies gracefully' do
+ namespaces.create!(id: 5, name: 'test1', path: 'test1')
+ test2 = namespaces.create!(id: 7, name: 'test2', path: 'test2')
+ namespaces.create!(id: 8, name: 'test3', path: 'test3', parent_id: 7)
+ namespaces.create!(id: 9, name: 'test4', path: 'test4')
+
+ # Simulate a situation where a user has had a chance to move a group to another parent
+ # before the background migration has had a chance to run
+ test2.update!(parent_id: 5)
+ ci_namespace_mirrors.create!(namespace_id: test2.id, traversal_ids: [5, 7])
+
+ subject.perform(5, 8)
+
+ expect(ci_namespace_mirrors.all).to contain_exactly(
+ an_object_having_attributes(namespace_id: 5, traversal_ids: [5]),
+ an_object_having_attributes(namespace_id: 7, traversal_ids: [5, 7]),
+ an_object_having_attributes(namespace_id: 8, traversal_ids: [5, 7, 8])
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb
new file mode 100644
index 00000000000..4eec83879e3
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_ci_project_mirrors_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillCiProjectMirrors, :migration, schema: 20211208122201 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:ci_project_mirrors) { table(:ci_project_mirrors) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'creates ci_project_mirrors for all projects in range' do
+ namespaces.create!(id: 10, name: 'namespace1', path: 'namespace1')
+ projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1')
+ projects.create!(id: 7, namespace_id: 10, name: 'test2', path: 'test2')
+ projects.create!(id: 8, namespace_id: 10, name: 'test3', path: 'test3')
+
+ subject.perform(5, 7)
+
+ expect(ci_project_mirrors.all).to contain_exactly(
+ an_object_having_attributes(project_id: 5, namespace_id: 10),
+ an_object_having_attributes(project_id: 7, namespace_id: 10)
+ )
+ end
+
+ it 'handles existing ci_project_mirrors gracefully' do
+ namespaces.create!(id: 10, name: 'namespace1', path: 'namespace1')
+ namespaces.create!(id: 11, name: 'namespace2', path: 'namespace2', parent_id: 10)
+ projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1')
+ projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2')
+ projects.create!(id: 8, namespace_id: 11, name: 'test3', path: 'test3')
+
+ # Simulate a situation where a user has had a chance to move a project to another namespace
+ # before the background migration has had a chance to run
+ ci_project_mirrors.create!(project_id: 7, namespace_id: 10)
+
+ subject.perform(5, 7)
+
+ expect(ci_project_mirrors.all).to contain_exactly(
+ an_object_having_attributes(project_id: 5, namespace_id: 10),
+ an_object_having_attributes(project_id: 7, namespace_id: 10)
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb b/spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb
new file mode 100644
index 00000000000..242da383453
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_incident_issue_escalation_statuses_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillIncidentIssueEscalationStatuses, schema: 20211214012507 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:issuable_escalation_statuses) { table(:incident_management_issuable_escalation_statuses) }
+
+ subject(:migration) { described_class.new }
+
+ it 'correctly backfills issuable escalation status records' do
+ namespace = namespaces.create!(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+
+ issues.create!(project_id: project.id, title: 'issue 1', issue_type: 0) # non-incident issue
+ issues.create!(project_id: project.id, title: 'incident 1', issue_type: 1)
+ issues.create!(project_id: project.id, title: 'incident 2', issue_type: 1)
+ incident_issue_existing_status = issues.create!(project_id: project.id, title: 'incident 3', issue_type: 1)
+ issuable_escalation_statuses.create!(issue_id: incident_issue_existing_status.id)
+
+ migration.perform(1, incident_issue_existing_status.id)
+
+ expect(issuable_escalation_statuses.count).to eq(3)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb b/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
index 446d62bbd2a..65f5f8368df 100644
--- a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillJiraTrackerDeploymentType2, :migration, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillJiraTrackerDeploymentType2, :migration, schema: 20210301200959 do
let_it_be(:jira_integration_temp) { described_class::JiraServiceTemp }
let_it_be(:jira_tracker_data_temp) { described_class::JiraTrackerDataTemp }
let_it_be(:atlassian_host) { 'https://api.atlassian.net' }
diff --git a/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
index 708e5e21dbe..ed44b819a97 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillProjectUpdatedAtAfterRepositoryStorageMove, :migration, schema: 20210210093901 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectUpdatedAtAfterRepositoryStorageMove, :migration, schema: 20210301200959 do
let(:projects) { table(:projects) }
let(:project_repository_storage_moves) { table(:project_repository_storage_moves) }
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/lib/gitlab/background_migration/base_job_spec.rb b/spec/lib/gitlab/background_migration/base_job_spec.rb
new file mode 100644
index 00000000000..86abe4257e4
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/base_job_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BaseJob, '#perform' do
+ let(:connection) { double(:connection) }
+
+ let(:test_job_class) { Class.new(described_class) }
+ let(:test_job) { test_job_class.new(connection: connection) }
+
+ describe '#perform' do
+ it 'raises an error if not overridden by a subclass' do
+ expect { test_job.perform }.to raise_error(NotImplementedError, /must implement perform/)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/cleanup_concurrent_schema_change_spec.rb b/spec/lib/gitlab/background_migration/cleanup_concurrent_schema_change_spec.rb
deleted file mode 100644
index 2931b5e6dd3..00000000000
--- a/spec/lib/gitlab/background_migration/cleanup_concurrent_schema_change_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::CleanupConcurrentSchemaChange do
- describe '#perform' do
- it 'new column does not exist' do
- expect(subject).to receive(:column_exists?).with(:issues, :closed_at_timestamp).and_return(false)
- expect(subject).not_to receive(:column_exists?).with(:issues, :closed_at)
- expect(subject).not_to receive(:define_model_for)
-
- expect(subject.perform(:issues, :closed_at, :closed_at_timestamp)).to be_nil
- end
-
- it 'old column does not exist' do
- expect(subject).to receive(:column_exists?).with(:issues, :closed_at_timestamp).and_return(true)
- expect(subject).to receive(:column_exists?).with(:issues, :closed_at).and_return(false)
- expect(subject).not_to receive(:define_model_for)
-
- expect(subject.perform(:issues, :closed_at, :closed_at_timestamp)).to be_nil
- end
-
- it 'has both old and new columns' do
- expect(subject).to receive(:column_exists?).twice.and_return(true)
-
- expect { subject.perform('issues', :closed_at, :created_at) }.to raise_error(NotImplementedError)
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
index b83dc6fff7a..5b6722a3384 100644
--- a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20210301200959 do
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let_it_be(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb
new file mode 100644
index 00000000000..94d9f4509a7
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::EncryptStaticObjectToken do
+ let(:users) { table(:users) }
+ let!(:user_without_tokens) { create_user!(name: 'notoken') }
+ let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') }
+ let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') }
+ let!(:user_with_plaintext_empty_token) { create_user!(name: 'plaintext_3', token: '') }
+ let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') }
+ let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') }
+
+ before do
+ allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).and_call_original
+ allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('token') { 'secure_token' }
+ allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('TOKEN') { 'SECURE_TOKEN' }
+ end
+
+ subject { described_class.new.perform(start_id, end_id) }
+
+ let(:start_id) { users.minimum(:id) }
+ let(:end_id) { users.maximum(:id) }
+
+ it 'backfills encrypted tokens to users with plaintext token only', :aggregate_failures do
+ subject
+
+ new_state = users.pluck(:id, :static_object_token, :static_object_token_encrypted).to_h do |row|
+ [row[0], [row[1], row[2]]]
+ end
+
+ expect(new_state.count).to eq(6)
+
+ expect(new_state[user_with_plaintext_token_1.id]).to match_array(%w[token secure_token])
+ expect(new_state[user_with_plaintext_token_2.id]).to match_array(%w[TOKEN SECURE_TOKEN])
+
+ expect(new_state[user_with_plaintext_empty_token.id]).to match_array(['', nil])
+ expect(new_state[user_without_tokens.id]).to match_array([nil, nil])
+ expect(new_state[user_with_both_tokens.id]).to match_array(%w[token2 encrypted2])
+ expect(new_state[user_with_encrypted_token.id]).to match_array([nil, 'encrypted'])
+ end
+
+ private
+
+ def create_user!(name:, token: nil, encrypted_token: nil)
+ email = "#{name}@example.com"
+
+ table(:users).create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ static_object_token: token,
+ static_object_token_encrypted: encrypted_token
+ )
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb b/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb
new file mode 100644
index 00000000000..af551861d47
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityOccurrencesWithHashesAsRawMetadata, schema: 20211209203821 do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let(:identifiers) { table(:vulnerability_identifiers) }
+ let(:findings) { table(:vulnerability_occurrences) }
+
+ let(:user) { users.create!(name: 'Test User', projects_limit: 10, username: 'test-user', email: '1') }
+
+ let(:namespace) do
+ namespaces.create!(
+ owner_id: user.id,
+ name: user.name,
+ path: user.username
+ )
+ end
+
+ let(:project) do
+ projects.create!(namespace_id: namespace.id, name: 'Test Project')
+ end
+
+ let(:scanner) do
+ scanners.create!(
+ project_id: project.id,
+ external_id: 'test-scanner',
+ name: 'Test Scanner',
+ vendor: 'GitLab'
+ )
+ end
+
+ let(:primary_identifier) do
+ identifiers.create!(
+ project_id: project.id,
+ external_type: 'cve',
+ name: 'CVE-2021-1234',
+ external_id: 'CVE-2021-1234',
+ fingerprint: '4c0fe491999f94701ee437588554ef56322ae276'
+ )
+ end
+
+ let(:finding) do
+ findings.create!(
+ raw_metadata: raw_metadata,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: primary_identifier.id,
+ uuid: '4deb090a-bedf-5ccc-aa9a-ac8055a1ea81',
+ project_fingerprint: '1caa750a6dad769a18ad6f40b413b3b6ab1c8d77',
+ location_fingerprint: '6d1f35f53b065238abfcadc01336ce65d112a2bd',
+ name: 'name',
+ report_type: 7,
+ severity: 0,
+ confidence: 0,
+ detection_method: 'gitlab_security_report',
+ metadata_version: 'cluster_image_scanning:1.0',
+ created_at: "2021-12-10 14:27:42 -0600",
+ updated_at: "2021-12-10 14:27:42 -0600"
+ )
+ end
+
+ subject(:perform) { described_class.new.perform(finding.id, finding.id) }
+
+ context 'with stringified hash as raw_metadata' do
+ let(:raw_metadata) do
+ '{:location=>{"image"=>"index.docker.io/library/nginx:latest", "kubernetes_resource"=>{"namespace"=>"production", "kind"=>"deployment", "name"=>"nginx", "container_name"=>"nginx", "agent_id"=>"2"}, "dependency"=>{"package"=>{"name"=>"libc"}, "version"=>"v1.2.3"}}}'
+ end
+
+ it 'converts stringified hash to JSON' do
+ expect { perform }.not_to raise_error
+
+ result = finding.reload.raw_metadata
+ metadata = Oj.load(result)
+ expect(metadata).to eq(
+ {
+ 'location' => {
+ 'image' => 'index.docker.io/library/nginx:latest',
+ 'kubernetes_resource' => {
+ 'namespace' => 'production',
+ 'kind' => 'deployment',
+ 'name' => 'nginx',
+ 'container_name' => 'nginx',
+ 'agent_id' => '2'
+ },
+ 'dependency' => {
+ 'package' => { 'name' => 'libc' },
+ 'version' => 'v1.2.3'
+ }
+ }
+ }
+ )
+ end
+ end
+
+ context 'with valid raw_metadata' do
+ where(:raw_metadata) do
+ [
+ '{}',
+ '{"location":null}',
+ '{"location":{"image":"index.docker.io/library/nginx:latest","kubernetes_resource":{"namespace":"production","kind":"deployment","name":"nginx","container_name":"nginx","agent_id":"2"},"dependency":{"package":{"name":"libc"},"version":"v1.2.3"}}}'
+ ]
+ end
+
+ with_them do
+ it 'does not change the raw_metadata' do
+ expect { perform }.not_to raise_error
+
+ result = finding.reload.raw_metadata
+ expect(result).to eq(raw_metadata)
+ end
+ end
+ end
+
+ context 'when raw_metadata contains forbidden types' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:raw_metadata, :type) do
+ 'def foo; "bar"; end' | :def
+ '`cat somefile`' | :xstr
+ 'exec("cat /etc/passwd")' | :send
+ end
+
+ with_them do
+ it 'does not change the raw_metadata' do
+ expect(Gitlab::AppLogger).to receive(:error).with(message: "expected raw_metadata to be a hash", type: type)
+
+ expect { perform }.not_to raise_error
+
+ result = finding.reload.raw_metadata
+ expect(result).to eq(raw_metadata)
+ end
+ end
+ end
+
+ context 'when forbidden types are nested inside a hash' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:raw_metadata, :type) do
+ '{:location=>Env.fetch("SOME_VAR")}' | :send
+ '{:location=>{:image=>Env.fetch("SOME_VAR")}}' | :send
+ # rubocop:disable Lint/InterpolationCheck
+ '{"key"=>"value: #{send}"}' | :dstr
+ # rubocop:enable Lint/InterpolationCheck
+ end
+
+ with_them do
+ it 'does not change the raw_metadata' do
+ expect(Gitlab::AppLogger).to receive(:error).with(
+ message: "error parsing raw_metadata",
+ error: "value of a pair was an unexpected type",
+ type: type
+ )
+
+ expect { perform }.not_to raise_error
+
+ result = finding.reload.raw_metadata
+ expect(result).to eq(raw_metadata)
+ end
+ end
+ end
+
+ context 'when key is an unexpected type' do
+ let(:raw_metadata) { "{nil=>nil}" }
+
+ it 'logs error' do
+ expect(Gitlab::AppLogger).to receive(:error).with(
+ message: "error parsing raw_metadata",
+ error: "expected key to be either symbol, string, or integer",
+ type: :nil
+ )
+
+ expect { perform }.not_to raise_error
+ end
+ end
+
+ context 'when raw_metadata cannot be parsed' do
+ let(:raw_metadata) { "{" }
+
+ it 'logs error' do
+ expect(Gitlab::AppLogger).to receive(:error).with(message: "error parsing raw_metadata", error: "unexpected token $end")
+
+ expect { perform }.not_to raise_error
+ end
+ end
+
+ describe '#hash_from_s' do
+ subject { described_class.new.hash_from_s(input) }
+
+ context 'with valid input' do
+ let(:input) { '{:location=>{"image"=>"index.docker.io/library/nginx:latest", "kubernetes_resource"=>{"namespace"=>"production", "kind"=>"deployment", "name"=>"nginx", "container_name"=>"nginx", "agent_id"=>2}, "dependency"=>{"package"=>{"name"=>"libc"}, "version"=>"v1.2.3"}}}' }
+
+ it 'converts string to a hash' do
+ expect(subject).to eq({
+ location: {
+ 'image' => 'index.docker.io/library/nginx:latest',
+ 'kubernetes_resource' => {
+ 'namespace' => 'production',
+ 'kind' => 'deployment',
+ 'name' => 'nginx',
+ 'container_name' => 'nginx',
+ 'agent_id' => 2
+ },
+ 'dependency' => {
+ 'package' => { 'name' => 'libc' },
+ 'version' => 'v1.2.3'
+ }
+ }
+ })
+ end
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:input, :expected) do
+ '{}' | {}
+ '{"bool"=>true}' | { 'bool' => true }
+ '{"bool"=>false}' | { 'bool' => false }
+ '{"nil"=>nil}' | { 'nil' => nil }
+ '{"array"=>[1, "foo", nil]}' | { 'array' => [1, "foo", nil] }
+ '{foo: :bar}' | { foo: :bar }
+ '{foo: {bar: "bin"}}' | { foo: { bar: "bin" } }
+ end
+
+ with_them do
+ specify { expect(subject).to eq(expected) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb
index 7a524d1489a..43d41408e66 100644
--- a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb
+++ b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb
@@ -202,23 +202,50 @@ RSpec.describe Gitlab::BackgroundMigration::JobCoordinator do
end
describe '#perform' do
- let(:migration) { spy(:migration) }
- let(:connection) { double('connection') }
+ let(:connection) { double(:connection) }
before do
- stub_const('Gitlab::BackgroundMigration::Foo', migration)
-
allow(coordinator).to receive(:connection).and_return(connection)
end
- it 'performs a background migration with the configured shared connection' do
- expect(coordinator).to receive(:with_shared_connection).and_call_original
+ context 'when the background migration does not inherit from BaseJob' do
+ let(:migration_class) { Class.new }
+
+ before do
+ stub_const('Gitlab::BackgroundMigration::Foo', migration_class)
+ end
+
+ it 'performs a background migration with the configured shared connection' do
+ expect(coordinator).to receive(:with_shared_connection).and_call_original
+
+ expect_next_instance_of(migration_class) do |migration|
+ expect(migration).to receive(:perform).with(10, 20).once do
+ expect(Gitlab::Database::SharedModel.connection).to be(connection)
+ end
+ end
+
+ coordinator.perform('Foo', [10, 20])
+ end
+ end
+
+ context 'when the background migration inherits from BaseJob' do
+ let(:migration_class) { Class.new(::Gitlab::BackgroundMigration::BaseJob) }
+ let(:migration) { double(:migration) }
- expect(migration).to receive(:perform).with(10, 20).once do
- expect(Gitlab::Database::SharedModel.connection).to be(connection)
+ before do
+ stub_const('Gitlab::BackgroundMigration::Foo', migration_class)
end
- coordinator.perform('Foo', [10, 20])
+ it 'passes the correct connection when constructing the migration' do
+ expect(coordinator).to receive(:with_shared_connection).and_call_original
+
+ expect(migration_class).to receive(:new).with(connection: connection).and_return(migration)
+ expect(migration).to receive(:perform).with(10, 20).once do
+ expect(Gitlab::Database::SharedModel.connection).to be(connection)
+ end
+
+ coordinator.perform('Foo', [10, 20])
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_legacy_artifacts_spec.rb b/spec/lib/gitlab/background_migration/migrate_legacy_artifacts_spec.rb
deleted file mode 100644
index 5c93e69b5e5..00000000000
--- a/spec/lib/gitlab/background_migration/migrate_legacy_artifacts_spec.rb
+++ /dev/null
@@ -1,158 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::MigrateLegacyArtifacts, schema: 20210210093901 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:pipelines) { table(:ci_pipelines) }
- let(:jobs) { table(:ci_builds) }
- let(:job_artifacts) { table(:ci_job_artifacts) }
-
- subject { described_class.new.perform(*range) }
-
- context 'when a pipeline exists' do
- let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let!(:project) { projects.create!(name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id) }
- let!(:pipeline) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a') }
-
- context 'when a legacy artifacts exists' do
- let(:artifacts_expire_at) { 1.day.since.to_s }
- let(:file_store) { ::ObjectStorage::Store::REMOTE }
-
- let!(:job) do
- jobs.create!(
- commit_id: pipeline.id,
- project_id: project.id,
- status: :success,
- **artifacts_archive_attributes,
- **artifacts_metadata_attributes)
- end
-
- let(:artifacts_archive_attributes) do
- {
- artifacts_file: 'archive.zip',
- artifacts_file_store: file_store,
- artifacts_size: 123,
- artifacts_expire_at: artifacts_expire_at
- }
- end
-
- let(:artifacts_metadata_attributes) do
- {
- artifacts_metadata: 'metadata.gz',
- artifacts_metadata_store: file_store
- }
- end
-
- it 'has legacy artifacts' do
- expect(jobs.pluck('artifacts_file, artifacts_file_store, artifacts_size, artifacts_expire_at')).to eq([artifacts_archive_attributes.values])
- expect(jobs.pluck('artifacts_metadata, artifacts_metadata_store')).to eq([artifacts_metadata_attributes.values])
- end
-
- it 'does not have new artifacts yet' do
- expect(job_artifacts.count).to be_zero
- end
-
- context 'when the record exists inside of the range of a background migration' do
- let(:range) { [job.id, job.id] }
-
- it 'migrates a legacy artifact to ci_job_artifacts table' do
- expect { subject }.to change { job_artifacts.count }.by(2)
-
- expect(job_artifacts.order(:id).pluck('project_id, job_id, file_type, file_store, size, expire_at, file, file_sha256, file_location'))
- .to eq([[project.id,
- job.id,
- described_class::ARCHIVE_FILE_TYPE,
- file_store,
- artifacts_archive_attributes[:artifacts_size],
- artifacts_expire_at,
- 'archive.zip',
- nil,
- described_class::LEGACY_PATH_FILE_LOCATION],
- [project.id,
- job.id,
- described_class::METADATA_FILE_TYPE,
- file_store,
- nil,
- artifacts_expire_at,
- 'metadata.gz',
- nil,
- described_class::LEGACY_PATH_FILE_LOCATION]])
-
- expect(jobs.pluck('artifacts_file, artifacts_file_store, artifacts_size, artifacts_expire_at')).to eq([[nil, nil, nil, artifacts_expire_at]])
- expect(jobs.pluck('artifacts_metadata, artifacts_metadata_store')).to eq([[nil, nil]])
- end
-
- context 'when file_store is nil' do
- let(:file_store) { nil }
-
- it 'has nullified file_store in all legacy artifacts' do
- expect(jobs.pluck('artifacts_file_store, artifacts_metadata_store')).to eq([[nil, nil]])
- end
-
- it 'fills file_store by the value of local file store' do
- subject
-
- expect(job_artifacts.pluck('file_store')).to all(eq(::ObjectStorage::Store::LOCAL))
- end
- end
-
- context 'when new artifacts has already existed' do
- context 'when only archive.zip existed' do
- before do
- job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: described_class::ARCHIVE_FILE_TYPE, size: 999, file: 'archive.zip')
- end
-
- it 'had archive.zip already' do
- expect(job_artifacts.exists?(job_id: job.id, file_type: described_class::ARCHIVE_FILE_TYPE)).to be_truthy
- end
-
- it 'migrates metadata' do
- expect { subject }.to change { job_artifacts.count }.by(1)
-
- expect(job_artifacts.exists?(job_id: job.id, file_type: described_class::METADATA_FILE_TYPE)).to be_truthy
- end
- end
-
- context 'when both archive and metadata existed' do
- before do
- job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: described_class::ARCHIVE_FILE_TYPE, size: 999, file: 'archive.zip')
- job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: described_class::METADATA_FILE_TYPE, size: 999, file: 'metadata.zip')
- end
-
- it 'does not migrate' do
- expect { subject }.not_to change { job_artifacts.count }
- end
- end
- end
- end
-
- context 'when the record exists outside of the range of a background migration' do
- let(:range) { [job.id + 1, job.id + 1] }
-
- it 'does not migrate' do
- expect { subject }.not_to change { job_artifacts.count }
- end
- end
- end
-
- context 'when the job does not have legacy artifacts' do
- let!(:job) { jobs.create!(commit_id: pipeline.id, project_id: project.id, status: :success) }
-
- it 'does not have the legacy artifacts in database' do
- expect(jobs.count).to eq(1)
- expect(jobs.pluck('artifacts_file, artifacts_file_store, artifacts_size, artifacts_expire_at')).to eq([[nil, nil, nil, nil]])
- expect(jobs.pluck('artifacts_metadata, artifacts_metadata_store')).to eq([[nil, nil]])
- end
-
- context 'when the record exists inside of the range of a background migration' do
- let(:range) { [job.id, job.id] }
-
- it 'does not migrate' do
- expect { subject }.not_to change { job_artifacts.count }
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
index ab183d01357..fc957a7c425 100644
--- a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'webauthn/u2f_migrator'
-RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20210301200959 do
let(:users) { table(:users) }
let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
diff --git a/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb b/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb
index b34a57f51f1..79b5567f5b3 100644
--- a/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb
+++ b/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::MoveContainerRegistryEnabledToProjectFeature, :migration, schema: 2021_02_26_120851 do
+RSpec.describe Gitlab::BackgroundMigration::MoveContainerRegistryEnabledToProjectFeature, :migration, schema: 20210301200959 do
let(:enabled) { 20 }
let(:disabled) { 0 }
diff --git a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
index 25006e663ab..68fe8f39f59 100644
--- a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityFeedback, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityFeedback, schema: 20210301200959 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
index a03a11489b5..b00eb185b34 100644
--- a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20210301200959 do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
let!(:issue1) { table(:issues).create!(id: 1, project_id: project.id, service_desk_reply_to: "a@gitlab.com") }
diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
index 4cdb56d3d3b..a54c840dd8e 100644
--- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -2,82 +2,124 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, schema: 20181228175414 do
+def create_background_migration_job(ids, status)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: Time.now.utc
+ )
+end
+
+RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, schema: 20211124132705 do
+ let(:background_migration_jobs) { table(:background_migration_jobs) }
+ let(:pending_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']) }
+ let(:succeeded_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']) }
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
let(:scanners) { table(:vulnerability_scanners) }
let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
let(:vulnerabilities) { table(:vulnerabilities) }
- let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
+ let(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let(:vulnerability_identifier) do
+ let(:identifier_1) { 'identifier-1' }
+ let!(:vulnerability_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
- external_type: 'uuid-v5',
- external_id: 'uuid-v5',
- fingerprint: Gitlab::Database::ShaAttribute.serialize('7e394d1b1eb461a7406d7b1e08f057a1cf11287a'),
- name: 'Identifier for UUIDv5')
+ external_type: identifier_1,
+ external_id: identifier_1,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('ff9ef548a6e30a0462795d916f3f00d1e2b082ca'),
+ name: 'Identifier 1')
end
- let(:different_vulnerability_identifier) do
+ let(:identifier_2) { 'identifier-2' }
+ let!(:vulnerability_identfier2) do
vulnerability_identifiers.create!(
project_id: project.id,
- external_type: 'uuid-v4',
- external_id: 'uuid-v4',
- fingerprint: Gitlab::Database::ShaAttribute.serialize('772da93d34a1ba010bcb5efa9fb6f8e01bafcc89'),
- name: 'Identifier for UUIDv4')
+ external_type: identifier_2,
+ external_id: identifier_2,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('4299e8ddd819f9bde9cfacf45716724c17b5ddf7'),
+ name: 'Identifier 2')
end
- let!(:vulnerability_for_uuidv4) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id
- )
- end
-
- let!(:vulnerability_for_uuidv5) do
- create_vulnerability!(
+ let(:identifier_3) { 'identifier-3' }
+ let!(:vulnerability_identifier3) do
+ vulnerability_identifiers.create!(
project_id: project.id,
- author_id: user.id
- )
+ external_type: identifier_3,
+ external_id: identifier_3,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('8e91632f9c6671e951834a723ee221c44cc0d844'),
+ name: 'Identifier 3')
end
- let(:known_uuid_v5) { "77211ed6-7dff-5f6b-8c9a-da89ad0a9b60" }
let(:known_uuid_v4) { "b3cc2518-5446-4dea-871c-89d5e999c1ac" }
- let(:desired_uuid_v5) { "3ca8ad45-6344-508b-b5e3-306a3bd6c6ba" }
+ let(:known_uuid_v5) { "05377088-dc26-5161-920e-52a7159fdaa1" }
+ let(:desired_uuid_v5) { "f3e9a23f-9181-54bf-a5ab-c5bc7a9b881a" }
- subject { described_class.new.perform(finding.id, finding.id) }
+ subject { described_class.new.perform(start_id, end_id) }
+
+ context 'when the migration is disabled by the feature flag' do
+ let(:start_id) { 1 }
+ let(:end_id) { 1001 }
+
+ before do
+ stub_feature_flags(migrate_vulnerability_finding_uuids: false)
+ end
+
+ it 'logs the info message and does not run the migration' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).once.with(message: 'Migration is disabled by the feature flag',
+ migrator: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ start_id: start_id,
+ end_id: end_id)
+ end
+
+ subject
+ end
+ end
context "when finding has a UUIDv4" do
before do
@uuid_v4 = create_finding!(
- vulnerability_id: vulnerability_for_uuidv4.id,
+ vulnerability_id: nil,
project_id: project.id,
- scanner_id: different_scanner.id,
- primary_identifier_id: different_vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identfier2.id,
report_type: 0, # "sast"
location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4
)
end
- let(:finding) { @uuid_v4 }
+ let(:start_id) { @uuid_v4.id }
+ let(:end_id) { @uuid_v4.id }
it "replaces it with UUIDv5" do
- expect(vulnerabilities_findings.pluck(:uuid)).to eq([known_uuid_v4])
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v4])
subject
- expect(vulnerabilities_findings.pluck(:uuid)).to eq([desired_uuid_v5])
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([desired_uuid_v5])
end
it 'logs recalculation' do
expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
- expect(instance).to receive(:info).once
+ expect(instance).to receive(:info).twice
end
subject
@@ -87,7 +129,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
context "when finding has a UUIDv5" do
before do
@uuid_v5 = create_finding!(
- vulnerability_id: vulnerability_for_uuidv5.id,
+ vulnerability_id: nil,
project_id: project.id,
scanner_id: scanner.id,
primary_identifier_id: vulnerability_identifier.id,
@@ -97,40 +139,340 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
)
end
- let(:finding) { @uuid_v5 }
+ let(:start_id) { @uuid_v5.id }
+ let(:end_id) { @uuid_v5.id }
it "stays the same" do
- expect(vulnerabilities_findings.pluck(:uuid)).to eq([known_uuid_v5])
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v5])
subject
- expect(vulnerabilities_findings.pluck(:uuid)).to eq([known_uuid_v5])
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v5])
+ end
+ end
+
+ context 'if a duplicate UUID would be generated' do # rubocop: disable RSpec/MultipleMemoizedHelpers
+ let(:v1) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_incorrect_uuid) do
+ create_finding!(
+ vulnerability_id: v1.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: 'bd95c085-71aa-51d7-9bb6-08ae669c262e'
+ )
+ end
+
+ let(:v2) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_correct_uuid) do
+ create_finding!(
+ vulnerability_id: v2.id,
+ project_id: project.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '91984483-5efe-5215-b471-d524ac5792b1'
+ )
+ end
+
+ let(:v3) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_incorrect_uuid2) do
+ create_finding!(
+ vulnerability_id: v3.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '00000000-1111-2222-3333-444444444444'
+ )
+ end
+
+ let(:v4) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_correct_uuid2) do
+ create_finding!(
+ vulnerability_id: v4.id,
+ project_id: project.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '1edd751e-ef9a-5391-94db-a832c8635bfc'
+ )
+ end
+
+ let!(:finding_with_incorrect_uuid3) do
+ create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier3.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '22222222-3333-4444-5555-666666666666'
+ )
+ end
+
+ let!(:duplicate_not_in_the_same_batch) do
+ create_finding!(
+ id: 99999,
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identifier3.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '4564f9d5-3c6b-5cc3-af8c-7c25285362a7'
+ )
+ end
+
+ let(:start_id) { finding_with_incorrect_uuid.id }
+ let(:end_id) { finding_with_incorrect_uuid3.id }
+
+ before do
+ 4.times do
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_incorrect_uuid.id)
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_correct_uuid.id)
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_incorrect_uuid2.id)
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_correct_uuid2.id)
+ end
+ end
+
+ it 'drops duplicates and related records', :aggregate_failures do
+ expect(vulnerability_findings.pluck(:id)).to match_array([
+ finding_with_correct_uuid.id, finding_with_incorrect_uuid.id, finding_with_correct_uuid2.id, finding_with_incorrect_uuid2.id, finding_with_incorrect_uuid3.id, duplicate_not_in_the_same_batch.id
+ ])
+
+ expect { subject }.to change(vulnerability_finding_pipelines, :count).from(16).to(8)
+ .and change(vulnerability_findings, :count).from(6).to(3)
+ .and change(vulnerabilities, :count).from(4).to(2)
+
+ expect(vulnerability_findings.pluck(:id)).to match_array([finding_with_incorrect_uuid.id, finding_with_incorrect_uuid2.id, finding_with_incorrect_uuid3.id])
+ end
+
+ context 'if there are conflicting UUID values within the batch' do # rubocop: disable RSpec/MultipleMemoizedHelpers
+ let(:end_id) { finding_with_broken_data_integrity.id }
+ let(:vulnerability_5) { create_vulnerability!(project_id: project.id, author_id: user.id) }
+ let(:different_project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:identifier_with_broken_data_integrity) do
+ vulnerability_identifiers.create!(
+ project_id: different_project.id,
+ external_type: identifier_2,
+ external_id: identifier_2,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('4299e8ddd819f9bde9cfacf45716724c17b5ddf7'),
+ name: 'Identifier 2')
+ end
+
+ let(:finding_with_broken_data_integrity) do
+ create_finding!(
+ vulnerability_id: vulnerability_5,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier_with_broken_data_integrity.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: SecureRandom.uuid
+ )
+ end
+
+ it 'deletes the conflicting record' do
+ expect { subject }.to change { vulnerability_findings.find_by_id(finding_with_broken_data_integrity.id) }.to(nil)
+ end
+ end
+
+ context 'if a conflicting UUID is found during the migration' do # rubocop:disable RSpec/MultipleMemoizedHelpers
+ let(:finding_class) { Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid::VulnerabilitiesFinding }
+ let(:uuid) { '4564f9d5-3c6b-5cc3-af8c-7c25285362a7' }
+
+ before do
+ exception = ActiveRecord::RecordNotUnique.new("(uuid)=(#{uuid})")
+
+ call_count = 0
+ allow(::Gitlab::Database::BulkUpdate).to receive(:execute) do
+ call_count += 1
+ call_count.eql?(1) ? raise(exception) : {}
+ end
+
+ allow(finding_class).to receive(:find_by).with(uuid: uuid).and_return(duplicate_not_in_the_same_batch)
+ end
+
+ it 'retries the recalculation' do
+ subject
+
+ expect(Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid::VulnerabilitiesFinding).to have_received(:find_by).with(uuid: uuid).once
+ end
+
+ it 'logs the conflict' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).exactly(6).times
+ end
+
+ subject
+ end
+
+ it 'marks the job as done' do
+ create_background_migration_job([start_id, end_id], :pending)
+
+ subject
+
+ expect(pending_jobs.count).to eq(0)
+ expect(succeeded_jobs.count).to eq(1)
+ end
+ end
+
+ it 'logs an exception if a different uniquness problem was found' do
+ exception = ActiveRecord::RecordNotUnique.new("Totally not an UUID uniqueness problem")
+ allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(exception)
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
+
+ subject
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(exception).once
+ end
+
+ it 'logs a duplicate found message' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).exactly(3).times
+ end
+
+ subject
+ end
+ end
+
+ context 'when finding has a signature' do
+ before do
+ @f1 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: 'd15d774d-e4b1-5a1b-929b-19f2a53e35ec'
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f1.id,
+ algorithm_type: 2, # location
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('57d4e05205f6462a73f039a5b2751aa1ab344e6e') # sha1('youshouldusethis')
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f1.id,
+ algorithm_type: 1, # hash
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('c554d8d8df1a7a14319eafdaae24af421bf5b587') # sha1('andnotthis')
+ )
+
+ @f2 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '4be029b5-75e5-5ac0-81a2-50ab41726135'
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f2.id,
+ algorithm_type: 2, # location
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('57d4e05205f6462a73f039a5b2751aa1ab344e6e') # sha1('youshouldusethis')
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f2.id,
+ algorithm_type: 1, # hash
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('c554d8d8df1a7a14319eafdaae24af421bf5b587') # sha1('andnotthis')
+ )
+ end
+
+ let(:start_id) { @f1.id }
+ let(:end_id) { @f2.id }
+
+ let(:uuids_before) { [@f1.uuid, @f2.uuid] }
+ let(:uuids_after) { %w[d3b60ddd-d312-5606-b4d3-ad058eebeacb 349d9bec-c677-5530-a8ac-5e58889c3b1a] }
+
+ it 'is recalculated using signature' do
+ expect(vulnerability_findings.pluck(:uuid)).to match_array(uuids_before)
+
+ subject
+
+ expect(vulnerability_findings.pluck(:uuid)).to match_array(uuids_after)
+ end
+ end
+
+ context 'if all records are removed before the job ran' do
+ let(:start_id) { 1 }
+ let(:end_id) { 9 }
+
+ before do
+ create_background_migration_job([start_id, end_id], :pending)
+ end
+
+ it 'does not error out' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'marks the job as done' do
+ subject
+
+ expect(pending_jobs.count).to eq(0)
+ expect(succeeded_jobs.count).to eq(1)
end
end
context 'when recalculation fails' do
before do
@uuid_v4 = create_finding!(
- vulnerability_id: vulnerability_for_uuidv4.id,
+ vulnerability_id: nil,
project_id: project.id,
- scanner_id: different_scanner.id,
- primary_identifier_id: different_vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identfier2.id,
report_type: 0, # "sast"
location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4
)
- allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(expected_error)
end
- let(:finding) { @uuid_v4 }
+ let(:start_id) { @uuid_v4.id }
+ let(:end_id) { @uuid_v4.id }
let(:expected_error) { RuntimeError.new }
it 'captures the errors and does not crash entirely' do
expect { subject }.not_to raise_error
- expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(expected_error).once
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(expected_error).once
end
end
@@ -149,25 +491,28 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
# rubocop:disable Metrics/ParameterLists
def create_finding!(
+ id: nil,
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
- vulnerabilities_findings.create!(
- vulnerability_id: vulnerability_id,
- project_id: project_id,
- name: name,
- severity: severity,
- confidence: confidence,
- report_type: report_type,
- project_fingerprint: project_fingerprint,
- scanner_id: scanner.id,
- primary_identifier_id: vulnerability_identifier.id,
- location_fingerprint: location_fingerprint,
- metadata_version: metadata_version,
- raw_metadata: raw_metadata,
- uuid: uuid
- )
+ vulnerability_findings.create!({
+ id: id,
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ }.compact
+ )
end
# rubocop:enable Metrics/ParameterLists
@@ -181,4 +526,9 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
confirmed_at: confirmed_at
)
end
+
+ def create_finding_pipeline!(project_id:, finding_id:)
+ pipeline = table(:ci_pipelines).create!(project_id: project_id)
+ vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id)
+ end
end
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb
deleted file mode 100644
index afcdaaf1cb8..00000000000
--- a/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateServices, :migration, schema: 20181228175414 do
- let_it_be(:users) { table(:users) }
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:services) { table(:services) }
-
- let_it_be(:alerts_service_data) { table(:alerts_service_data) }
- let_it_be(:chat_names) { table(:chat_names) }
- let_it_be(:issue_tracker_data) { table(:issue_tracker_data) }
- let_it_be(:jira_tracker_data) { table(:jira_tracker_data) }
- let_it_be(:open_project_tracker_data) { table(:open_project_tracker_data) }
- let_it_be(:slack_integrations) { table(:slack_integrations) }
- let_it_be(:web_hooks) { table(:web_hooks) }
-
- let_it_be(:data_tables) do
- [alerts_service_data, chat_names, issue_tracker_data, jira_tracker_data, open_project_tracker_data, slack_integrations, web_hooks]
- end
-
- let!(:user) { users.create!(id: 1, projects_limit: 100) }
- let!(:namespace) { namespaces.create!(id: 1, name: 'group', path: 'group') }
-
- # project without duplicate services
- let!(:project1) { projects.create!(id: 1, namespace_id: namespace.id) }
- let!(:service1) { services.create!(id: 1, project_id: project1.id, type: 'AsanaService') }
- let!(:service2) { services.create!(id: 2, project_id: project1.id, type: 'JiraService') }
- let!(:service3) { services.create!(id: 3, project_id: project1.id, type: 'SlackService') }
-
- # project with duplicate services
- let!(:project2) { projects.create!(id: 2, namespace_id: namespace.id) }
- let!(:service4) { services.create!(id: 4, project_id: project2.id, type: 'AsanaService') }
- let!(:service5) { services.create!(id: 5, project_id: project2.id, type: 'JiraService') }
- let!(:service6) { services.create!(id: 6, project_id: project2.id, type: 'JiraService') }
- let!(:service7) { services.create!(id: 7, project_id: project2.id, type: 'SlackService') }
- let!(:service8) { services.create!(id: 8, project_id: project2.id, type: 'SlackService') }
- let!(:service9) { services.create!(id: 9, project_id: project2.id, type: 'SlackService') }
-
- # project with duplicate services and dependant records
- let!(:project3) { projects.create!(id: 3, namespace_id: namespace.id) }
- let!(:service10) { services.create!(id: 10, project_id: project3.id, type: 'AlertsService') }
- let!(:service11) { services.create!(id: 11, project_id: project3.id, type: 'AlertsService') }
- let!(:service12) { services.create!(id: 12, project_id: project3.id, type: 'SlashCommandsService') }
- let!(:service13) { services.create!(id: 13, project_id: project3.id, type: 'SlashCommandsService') }
- let!(:service14) { services.create!(id: 14, project_id: project3.id, type: 'IssueTrackerService') }
- let!(:service15) { services.create!(id: 15, project_id: project3.id, type: 'IssueTrackerService') }
- let!(:service16) { services.create!(id: 16, project_id: project3.id, type: 'JiraService') }
- let!(:service17) { services.create!(id: 17, project_id: project3.id, type: 'JiraService') }
- let!(:service18) { services.create!(id: 18, project_id: project3.id, type: 'OpenProjectService') }
- let!(:service19) { services.create!(id: 19, project_id: project3.id, type: 'OpenProjectService') }
- let!(:service20) { services.create!(id: 20, project_id: project3.id, type: 'SlackService') }
- let!(:service21) { services.create!(id: 21, project_id: project3.id, type: 'SlackService') }
- let!(:dependant_records) do
- alerts_service_data.create!(id: 1, service_id: service10.id)
- alerts_service_data.create!(id: 2, service_id: service11.id)
- chat_names.create!(id: 1, service_id: service12.id, user_id: user.id, team_id: 'team1', chat_id: 'chat1')
- chat_names.create!(id: 2, service_id: service13.id, user_id: user.id, team_id: 'team2', chat_id: 'chat2')
- issue_tracker_data.create!(id: 1, service_id: service14.id)
- issue_tracker_data.create!(id: 2, service_id: service15.id)
- jira_tracker_data.create!(id: 1, service_id: service16.id)
- jira_tracker_data.create!(id: 2, service_id: service17.id)
- open_project_tracker_data.create!(id: 1, service_id: service18.id)
- open_project_tracker_data.create!(id: 2, service_id: service19.id)
- slack_integrations.create!(id: 1, service_id: service20.id, user_id: user.id, team_id: 'team1', team_name: 'team1', alias: 'alias1')
- slack_integrations.create!(id: 2, service_id: service21.id, user_id: user.id, team_id: 'team2', team_name: 'team2', alias: 'alias2')
- web_hooks.create!(id: 1, service_id: service20.id)
- web_hooks.create!(id: 2, service_id: service21.id)
- end
-
- # project without services
- let!(:project4) { projects.create!(id: 4, namespace_id: namespace.id) }
-
- it 'removes duplicate services and dependant records' do
- # Determine which services we expect to keep
- expected_services = projects.pluck(:id).each_with_object({}) do |project_id, map|
- project_services = services.where(project_id: project_id)
- types = project_services.distinct.pluck(:type)
-
- map[project_id] = types.map { |type| project_services.where(type: type).take!.id }
- end
-
- expect do
- subject.perform(project2.id, project3.id)
- end.to change { services.count }.from(21).to(12)
-
- services1 = services.where(project_id: project1.id)
- expect(services1.count).to be(3)
- expect(services1.pluck(:type)).to contain_exactly('AsanaService', 'JiraService', 'SlackService')
- expect(services1.pluck(:id)).to contain_exactly(*expected_services[project1.id])
-
- services2 = services.where(project_id: project2.id)
- expect(services2.count).to be(3)
- expect(services2.pluck(:type)).to contain_exactly('AsanaService', 'JiraService', 'SlackService')
- expect(services2.pluck(:id)).to contain_exactly(*expected_services[project2.id])
-
- services3 = services.where(project_id: project3.id)
- expect(services3.count).to be(6)
- expect(services3.pluck(:type)).to contain_exactly('AlertsService', 'SlashCommandsService', 'IssueTrackerService', 'JiraService', 'OpenProjectService', 'SlackService')
- expect(services3.pluck(:id)).to contain_exactly(*expected_services[project3.id])
-
- kept_services = expected_services.values.flatten
- data_tables.each do |table|
- expect(table.count).to be(1)
- expect(kept_services).to include(table.pluck(:service_id).first)
- end
- end
-
- it 'does not delete services without duplicates' do
- expect do
- subject.perform(project1.id, project4.id)
- end.not_to change { services.count }
- end
-
- it 'only deletes duplicate services for the current batch' do
- expect do
- subject.perform(project2.id)
- end.to change { services.count }.by(-3)
- end
-end
diff --git a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
index fadee64886f..ccf96e036ae 100644
--- a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
@@ -41,8 +41,8 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveVulnerabilityFindingLinks, :mi
# vulnerability finding links
let!(:links) do
{
- findings.first => Array.new(5) { |id| finding_links.create!(vulnerability_occurrence_id: findings.first.id, name: "Link Name 1", url: "link_url1.example") },
- findings.second => Array.new(5) { |id| finding_links.create!(vulnerability_occurrence_id: findings.second.id, name: "Link Name 2", url: "link_url2.example") }
+ findings.first => Array.new(5) { |id| finding_links.create!(vulnerability_occurrence_id: findings.first.id, name: "Link Name 1", url: "link_url1_#{id}.example") },
+ findings.second => Array.new(5) { |id| finding_links.create!(vulnerability_occurrence_id: findings.second.id, name: "Link Name 2", url: "link_url2_#{id}.example") }
}
end
diff --git a/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb b/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
index 5c197526a55..17fe25c7f71 100644
--- a/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
+++ b/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::WrongfullyConfirmedEmailUnconfirmer, schema: 20181228175414 do
+RSpec.describe Gitlab::BackgroundMigration::WrongfullyConfirmedEmailUnconfirmer, schema: 20210301200959 do
let(:users) { table(:users) }
let(:emails) { table(:emails) }
let(:user_synced_attributes_metadata) { table(:user_synced_attributes_metadata) }
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
index 633c4baa931..1cb4edd7337 100644
--- a/spec/lib/gitlab/checks/changes_access_spec.rb
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -44,16 +44,30 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
it 'calls #new_commits' do
expect(project.repository).to receive(:new_commits).and_call_original
- expect(subject.commits).to eq([])
+ expect(subject.commits).to match_array([])
end
context 'when changes contain empty revisions' do
- let(:changes) { [{ newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] }
let(:expected_commit) { instance_double(Commit) }
- it 'returns only commits with non empty revisions' do
- expect(project.repository).to receive(:new_commits).with([newrev], { allow_quarantine: true }) { [expected_commit] }
- expect(subject.commits).to eq([expected_commit])
+ shared_examples 'returns only commits with non empty revisions' do
+ specify do
+ expect(project.repository).to receive(:new_commits).with([newrev], { allow_quarantine: allow_quarantine }) { [expected_commit] }
+ expect(subject.commits).to match_array([expected_commit])
+ end
+ end
+
+ it_behaves_like 'returns only commits with non empty revisions' do
+ let(:changes) { [{ oldrev: oldrev, newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] }
+ let(:allow_quarantine) { true }
+ end
+
+ context 'without oldrev' do
+ it_behaves_like 'returns only commits with non empty revisions' do
+ let(:changes) { [{ newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] }
+ # The quarantine directory should not be used because we're lacking oldrev.
+ let(:allow_quarantine) { false }
+ end
end
end
end
@@ -61,12 +75,13 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
describe '#commits_for' do
let(:new_commits) { [] }
let(:expected_commits) { [] }
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
shared_examples 'a listing of new commits' do
it 'returns expected commits' do
expect(subject).to receive(:commits).and_return(new_commits)
- expect(subject.commits_for(newrev)).to eq(expected_commits)
+ expect(subject.commits_for(oldrev, newrev)).to eq(expected_commits)
end
end
@@ -172,6 +187,31 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
it_behaves_like 'a listing of new commits'
end
+
+ context 'with over-push' do
+ let(:newrev) { '1' }
+ let(:oldrev) { '3' }
+
+ # `#new_commits` returns too many commits, where some commits are not
+ # part of the current change.
+ let(:new_commits) do
+ [
+ create_commit('1', %w[2]),
+ create_commit('2', %w[3]),
+ create_commit('3', %w[4]),
+ create_commit('4', %w[])
+ ]
+ end
+
+ let(:expected_commits) do
+ [
+ create_commit('1', %w[2]),
+ create_commit('2', %w[3])
+ ]
+ end
+
+ it_behaves_like 'a listing of new commits'
+ end
end
describe '#single_change_accesses' do
@@ -180,10 +220,10 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
shared_examples '#single_change_access' do
before do
- commits_for.each do |id, commits|
+ commits_for.each do |oldrev, newrev, commits|
expect(subject)
.to receive(:commits_for)
- .with(id)
+ .with(oldrev, newrev)
.and_return(commits)
end
end
@@ -205,7 +245,12 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
end
context 'with a single change and no new commits' do
- let(:commits_for) { { 'new' => [] } }
+ let(:commits_for) do
+ [
+ ['old', 'new', []]
+ ]
+ end
+
let(:changes) do
[
{ oldrev: 'old', newrev: 'new', ref: 'refs/heads/branch' }
@@ -222,7 +267,12 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
end
context 'with a single change and new commits' do
- let(:commits_for) { { 'new' => [create_commit('new', [])] } }
+ let(:commits_for) do
+ [
+ ['old', 'new', [create_commit('new', [])]]
+ ]
+ end
+
let(:changes) do
[
{ oldrev: 'old', newrev: 'new', ref: 'refs/heads/branch' }
@@ -240,11 +290,11 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
context 'with multiple changes' do
let(:commits_for) do
- {
- 'a' => [create_commit('a', [])],
- 'c' => [create_commit('c', [])],
- 'd' => []
- }
+ [
+ [nil, 'a', [create_commit('a', [])]],
+ ['a', 'c', [create_commit('c', [])]],
+ [nil, 'd', []]
+ ]
end
let(:changes) do
diff --git a/spec/lib/gitlab/ci/build/status/reason_spec.rb b/spec/lib/gitlab/ci/build/status/reason_spec.rb
new file mode 100644
index 00000000000..64f35c3f464
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/status/reason_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Build::Status::Reason do
+ let(:build) { double('build') }
+
+ describe '.fabricate' do
+ context 'when failure symbol reason is being passed' do
+ it 'correctly fabricates a status reason object' do
+ reason = described_class.fabricate(build, :script_failure)
+
+ expect(reason.failure_reason_enum).to eq 1
+ end
+ end
+
+ context 'when another status reason object is being passed' do
+ it 'correctly fabricates a status reason object' do
+ reason = described_class.fabricate(build, :script_failure)
+
+ new_reason = described_class.fabricate(build, reason)
+
+ expect(new_reason.failure_reason_enum).to eq 1
+ end
+ end
+ end
+
+ describe '#failure_reason_enum' do
+ it 'exposes a failure reason enum' do
+ reason = described_class.fabricate(build, :script_failure)
+
+ enum = ::CommitStatus.failure_reasons[:script_failure]
+
+ expect(reason.failure_reason_enum).to eq enum
+ end
+ end
+
+ describe '#force_allow_failure?' do
+ context 'when build is not allowed to fail' do
+ context 'when build is allowed to fail with a given exit code' do
+ it 'returns true' do
+ reason = described_class.new(build, :script_failure, 11)
+
+ allow(build).to receive(:allow_failure?).and_return(false)
+ allow(build).to receive(:allowed_to_fail_with_code?)
+ .with(11)
+ .and_return(true)
+
+ expect(reason.force_allow_failure?).to be true
+ end
+ end
+
+ context 'when build is not allowed to fail regardless of an exit code' do
+ it 'returns false' do
+ reason = described_class.new(build, :script_failure, 11)
+
+ allow(build).to receive(:allow_failure?).and_return(false)
+ allow(build).to receive(:allowed_to_fail_with_code?)
+ .with(11)
+ .and_return(false)
+
+ expect(reason.force_allow_failure?).to be false
+ end
+ end
+
+ context 'when an exit code is not specified' do
+ it 'returns false' do
+ reason = described_class.new(build, :script_failure)
+
+ expect(reason.force_allow_failure?).to be false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index d862fbf5b78..749d1386ed9 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Root do
- let(:root) { described_class.new(hash) }
+ let(:user) {}
+ let(:project) {}
+ let(:root) { described_class.new(hash, user: user, project: project) }
describe '.nodes' do
it 'returns a hash' do
@@ -53,6 +55,37 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
}
end
+ context 'when deprecated types keyword is defined' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ let(:hash) do
+ { types: %w(test deploy),
+ rspec: { script: 'rspec' } }
+ end
+
+ before do
+ root.compose!
+ end
+
+ it 'returns array of types as stages with a warning' do
+ expect(root.stages_value).to eq %w[test deploy]
+ expect(root.warnings).to match_array(["root `types` is deprecated in 9.0 and will be removed in 15.0."])
+ end
+
+ it 'logs usage of types keyword' do
+ expect(Gitlab::AppJsonLogger).to(
+ receive(:info)
+ .with(event: 'ci_used_deprecated_keyword',
+ entry: root[:stages].key.to_s,
+ user_id: user.id,
+ project_id: project.id)
+ )
+
+ root.compose!
+ end
+ end
+
describe '#compose!' do
before do
root.compose!
@@ -108,17 +141,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
expect(root.stages_value).to eq %w[build pages release]
end
end
-
- context 'when deprecated types key defined' do
- let(:hash) do
- { types: %w(test deploy),
- rspec: { script: 'rspec' } }
- end
-
- it 'returns array of types as stages' do
- expect(root.stages_value).to eq %w[test deploy]
- end
- end
end
describe '#jobs_value' do
diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb
new file mode 100644
index 00000000000..33aaa145a39
--- /dev/null
+++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::JwtV2 do
+ let(:namespace) { build_stubbed(:namespace) }
+ let(:project) { build_stubbed(:project, namespace: namespace) }
+ let(:user) { build_stubbed(:user) }
+ let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'auto-deploy-2020-03-19') }
+ let(:build) do
+ build_stubbed(
+ :ci_build,
+ project: project,
+ user: user,
+ pipeline: pipeline
+ )
+ end
+
+ subject(:ci_job_jwt_v2) { described_class.new(build, ttl: 30) }
+
+ it { is_expected.to be_a Gitlab::Ci::Jwt }
+
+ describe '#payload' do
+ subject(:payload) { ci_job_jwt_v2.payload }
+
+ it 'has correct values for the standard JWT attributes' do
+ aggregate_failures do
+ expect(payload[:iss]).to eq(Settings.gitlab.base_url)
+ expect(payload[:aud]).to eq(Settings.gitlab.base_url)
+ expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
index 28bc685286f..0a592395c3a 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
@@ -38,20 +38,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do
expect(job.deployment.environment).to eq(job.persisted_environment)
end
- context 'when creation failure occures' do
- before do
- allow_next_instance_of(Deployment) do |deployment|
- allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid }
- end
- end
-
- it 'trackes the exception' do
- expect { subject }.to raise_error(described_class::DeploymentCreationError)
-
- expect(Deployment.count).to eq(0)
- end
- end
-
context 'when the corresponding environment does not exist' do
let!(:environment) { }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index 4206483b228..1d020d3ea79 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
let_it_be(:user) { create(:user) }
let(:pipeline) do
- build(:ci_empty_pipeline, project: project, ref: 'master')
+ build(:ci_empty_pipeline, project: project, ref: 'master', user: user)
end
let(:command) do
@@ -59,7 +59,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
context 'tags persistence' do
let(:stage) do
- build(:ci_stage_entity, pipeline: pipeline)
+ build(:ci_stage_entity, pipeline: pipeline, project: project)
end
let(:job) do
@@ -79,12 +79,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
it 'extracts an empty tag list' do
expect(CommitStatus)
.to receive(:bulk_insert_tags!)
- .with(stage.statuses, {})
+ .with([job])
.and_call_original
step.perform!
- expect(job.instance_variable_defined?(:@tag_list)).to be_falsey
expect(job).to be_persisted
expect(job.tag_list).to eq([])
end
@@ -98,14 +97,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
it 'bulk inserts tags' do
expect(CommitStatus)
.to receive(:bulk_insert_tags!)
- .with(stage.statuses, { job.name => %w[tag1 tag2] })
+ .with([job])
.and_call_original
step.perform!
- expect(job.instance_variable_defined?(:@tag_list)).to be_falsey
expect(job).to be_persisted
- expect(job.tag_list).to match_array(%w[tag1 tag2])
+ expect(job.reload.tag_list).to match_array(%w[tag1 tag2])
end
end
@@ -120,7 +118,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
step.perform!
- expect(job.instance_variable_defined?(:@tag_list)).to be_truthy
expect(job).to be_persisted
expect(job.reload.tag_list).to match_array(%w[tag1 tag2])
end
diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
index 0b44e35dec1..a488bc184f8 100644
--- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
@@ -41,6 +41,90 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
end
end
+ describe '#instrument_with_sql', :request_store do
+ subject(:instrument_with_sql) do
+ logger.instrument_with_sql(:expensive_operation, &operation)
+ end
+
+ def loggable_data(count:, db_count: nil)
+ keys = %w[
+ expensive_operation_duration_s
+ expensive_operation_db_count
+ expensive_operation_db_primary_count
+ expensive_operation_db_primary_duration_s
+ expensive_operation_db_main_count
+ expensive_operation_db_main_duration_s
+ ]
+
+ data = keys.each.with_object({}) do |key, accumulator|
+ accumulator[key] = {
+ 'count' => count,
+ 'avg' => a_kind_of(Numeric),
+ 'max' => a_kind_of(Numeric),
+ 'min' => a_kind_of(Numeric)
+ }
+ end
+
+ if db_count
+ data['expensive_operation_db_count']['max'] = db_count
+ data['expensive_operation_db_count']['min'] = db_count
+ data['expensive_operation_db_count']['avg'] = db_count
+ end
+
+ data
+ end
+
+ context 'with a single query' do
+ let(:operation) { -> { Project.count } }
+
+ it { is_expected.to eq(operation.call) }
+
+ it 'includes SQL metrics' do
+ instrument_with_sql
+
+ expect(logger.observations_hash)
+ .to match(a_hash_including(loggable_data(count: 1, db_count: 1)))
+ end
+ end
+
+ context 'with multiple queries' do
+ let(:operation) { -> { Ci::Build.count + Ci::Bridge.count } }
+
+ it { is_expected.to eq(operation.call) }
+
+ it 'includes SQL metrics' do
+ instrument_with_sql
+
+ expect(logger.observations_hash)
+ .to match(a_hash_including(loggable_data(count: 1, db_count: 2)))
+ end
+ end
+
+ context 'with multiple observations' do
+ let(:operation) { -> { Ci::Build.count + Ci::Bridge.count } }
+
+ it 'includes SQL metrics' do
+ 2.times { logger.instrument_with_sql(:expensive_operation, &operation) }
+
+ expect(logger.observations_hash)
+ .to match(a_hash_including(loggable_data(count: 2, db_count: 2)))
+ end
+ end
+
+ context 'when there are not SQL operations' do
+ let(:operation) { -> { 123 } }
+
+ it { is_expected.to eq(operation.call) }
+
+ it 'does not include SQL metrics' do
+ instrument_with_sql
+
+ expect(logger.observations_hash.keys)
+ .to match_array(['expensive_operation_duration_s'])
+ end
+ end
+ end
+
describe '#observe' do
it 'records durations of observed operations' do
loggable_data = {
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 68806fbf287..2f9fcd7caac 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:pipeline) { build(:ci_empty_pipeline, project: project, sha: head_sha) }
let(:root_variables) { [] }
- let(:seed_context) { double(pipeline: pipeline, root_variables: root_variables) }
+ let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: root_variables) }
let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage, when: 'on_success' } }
let(:previous_stages) { [] }
let(:current_stage) { double(seeds_names: [attributes[:name]]) }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
index 5d8a9358e10..a76b4874eca 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let(:seed_context) { double(pipeline: pipeline, root_variables: []) }
+ let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: []) }
let(:stages_attributes) do
[
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index 5b04d2abd88..a632b5dedcf 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:previous_stages) { [] }
- let(:seed_context) { double(pipeline: pipeline, root_variables: []) }
+ let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: []) }
let(:attributes) do
{ name: 'test',
diff --git a/spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb b/spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb
new file mode 100644
index 00000000000..b703a8a47ac
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/waiting_for_approval_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Build::WaitingForApproval do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ subject { described_class.new(Gitlab::Ci::Status::Core.new(build, user)) }
+
+ describe '#illustration' do
+ let(:build) { create(:ci_build, :manual, environment: 'production', project: project) }
+
+ before do
+ environment = create(:environment, name: 'production', project: project)
+ create(:deployment, :blocked, project: project, environment: environment, deployable: build)
+ end
+
+ it { expect(subject.illustration).to include(:image, :size) }
+ it { expect(subject.illustration[:title]).to eq('Waiting for approval') }
+ it { expect(subject.illustration[:content]).to include('This job deploys to the protected environment "production"') }
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ let(:build) { create(:ci_build, :manual, environment: 'production', project: project) }
+
+ before do
+ create(:deployment, deployment_status, deployable: build, project: project)
+ end
+
+ context 'when build is waiting for approval' do
+ let(:deployment_status) { :blocked }
+
+ it 'is a correct match' do
+ expect(subject).to be_truthy
+ end
+ end
+
+ context 'when build is not waiting for approval' do
+ let(:deployment_status) { :created }
+
+ it 'does not match' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
index 6c1f56de840..6c4f69fb036 100644
--- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
+++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
@@ -5,27 +5,37 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Tags::BulkInsert do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let_it_be_with_refind(:job) { create(:ci_build, :unique_name, pipeline: pipeline, project: project) }
- let_it_be_with_refind(:other_job) { create(:ci_build, :unique_name, pipeline: pipeline, project: project) }
- let_it_be_with_refind(:bridge) { create(:ci_bridge, pipeline: pipeline, project: project) }
+ let_it_be_with_refind(:job) { create(:ci_build, :unique_name, pipeline: pipeline) }
+ let_it_be_with_refind(:other_job) { create(:ci_build, :unique_name, pipeline: pipeline) }
- let(:statuses) { [job, bridge, other_job] }
+ let(:statuses) { [job, other_job] }
- subject(:service) { described_class.new(statuses, tags_list) }
+ subject(:service) { described_class.new(statuses) }
+
+ describe 'gem version' do
+ let(:acceptable_version) { '9.0.0' }
+
+ let(:error_message) do
+ <<~MESSAGE
+ A mechanism depending on internals of 'act-as-taggable-on` has been designed
+ to bulk insert tags for Ci::Build records.
+ Please review the code carefully before updating the gem version
+ https://gitlab.com/gitlab-org/gitlab/-/issues/350053
+ MESSAGE
+ end
+
+ it { expect(ActsAsTaggableOn::VERSION).to eq(acceptable_version), error_message }
+ end
describe '#insert!' do
context 'without tags' do
- let(:tags_list) { {} }
-
it { expect(service.insert!).to be_falsey }
end
context 'with tags' do
- let(:tags_list) do
- {
- job.name => %w[tag1 tag2],
- other_job.name => %w[tag2 tag3 tag4]
- }
+ before do
+ job.tag_list = %w[tag1 tag2]
+ other_job.tag_list = %w[tag2 tag3 tag4]
end
it 'persists tags' do
@@ -35,5 +45,18 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
expect(other_job.reload.tag_list).to match_array(%w[tag2 tag3 tag4])
end
end
+
+ context 'with tags for only one job' do
+ before do
+ job.tag_list = %w[tag1 tag2]
+ end
+
+ it 'persists tags' do
+ expect(service.insert!).to be_truthy
+
+ expect(job.reload.tag_list).to match_array(%w[tag1 tag2])
+ expect(other_job.reload.tag_list).to be_empty
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb b/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb
index 8837ebc3652..1cd88034166 100644
--- a/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb
+++ b/spec/lib/gitlab/ci/trace/remote_checksum_spec.rb
@@ -30,14 +30,6 @@ RSpec.describe Gitlab::Ci::Trace::RemoteChecksum do
context 'with remote files' do
let(:file_store) { JobArtifactUploader::Store::REMOTE }
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(ci_archived_build_trace_checksum: false)
- end
-
- it { is_expected.to be_nil }
- end
-
context 'with AWS as provider' do
it { is_expected.to eq(checksum) }
end
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index 5ff34592b2f..8a87cbe45c1 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -3,25 +3,201 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Variables::Builder do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { project.owner }
+ let_it_be(:job) do
+ create(:ci_build,
+ pipeline: pipeline,
+ user: user,
+ yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }]
+ )
+ end
+
let(:builder) { described_class.new(pipeline) }
- let(:pipeline) { create(:ci_pipeline) }
- let(:job) { create(:ci_build, pipeline: pipeline) }
describe '#scoped_variables' do
let(:environment) { job.expanded_environment_name }
let(:dependencies) { true }
+ let(:predefined_variables) do
+ [
+ { key: 'CI_JOB_NAME',
+ value: job.name },
+ { key: 'CI_JOB_STAGE',
+ value: job.stage },
+ { key: 'CI_NODE_TOTAL',
+ value: '1' },
+ { key: 'CI_BUILD_NAME',
+ value: job.name },
+ { key: 'CI_BUILD_STAGE',
+ value: job.stage },
+ { key: 'CI',
+ value: 'true' },
+ { key: 'GITLAB_CI',
+ value: 'true' },
+ { key: 'CI_SERVER_URL',
+ value: Gitlab.config.gitlab.url },
+ { key: 'CI_SERVER_HOST',
+ value: Gitlab.config.gitlab.host },
+ { key: 'CI_SERVER_PORT',
+ value: Gitlab.config.gitlab.port.to_s },
+ { key: 'CI_SERVER_PROTOCOL',
+ value: Gitlab.config.gitlab.protocol },
+ { key: 'CI_SERVER_NAME',
+ value: 'GitLab' },
+ { key: 'CI_SERVER_VERSION',
+ value: Gitlab::VERSION },
+ { key: 'CI_SERVER_VERSION_MAJOR',
+ value: Gitlab.version_info.major.to_s },
+ { key: 'CI_SERVER_VERSION_MINOR',
+ value: Gitlab.version_info.minor.to_s },
+ { key: 'CI_SERVER_VERSION_PATCH',
+ value: Gitlab.version_info.patch.to_s },
+ { key: 'CI_SERVER_REVISION',
+ value: Gitlab.revision },
+ { key: 'GITLAB_FEATURES',
+ value: project.licensed_features.join(',') },
+ { key: 'CI_PROJECT_ID',
+ value: project.id.to_s },
+ { key: 'CI_PROJECT_NAME',
+ value: project.path },
+ { key: 'CI_PROJECT_TITLE',
+ value: project.title },
+ { key: 'CI_PROJECT_PATH',
+ value: project.full_path },
+ { key: 'CI_PROJECT_PATH_SLUG',
+ value: project.full_path_slug },
+ { key: 'CI_PROJECT_NAMESPACE',
+ value: project.namespace.full_path },
+ { key: 'CI_PROJECT_ROOT_NAMESPACE',
+ value: project.namespace.root_ancestor.path },
+ { key: 'CI_PROJECT_URL',
+ value: project.web_url },
+ { key: 'CI_PROJECT_VISIBILITY',
+ value: "private" },
+ { key: 'CI_PROJECT_REPOSITORY_LANGUAGES',
+ value: project.repository_languages.map(&:name).join(',').downcase },
+ { key: 'CI_PROJECT_CLASSIFICATION_LABEL',
+ value: project.external_authorization_classification_label },
+ { key: 'CI_DEFAULT_BRANCH',
+ value: project.default_branch },
+ { key: 'CI_CONFIG_PATH',
+ value: project.ci_config_path_or_default },
+ { key: 'CI_PAGES_DOMAIN',
+ value: Gitlab.config.pages.host },
+ { key: 'CI_PAGES_URL',
+ value: project.pages_url },
+ { key: 'CI_API_V4_URL',
+ value: API::Helpers::Version.new('v4').root_url },
+ { key: 'CI_PIPELINE_IID',
+ value: pipeline.iid.to_s },
+ { key: 'CI_PIPELINE_SOURCE',
+ value: pipeline.source },
+ { key: 'CI_PIPELINE_CREATED_AT',
+ value: pipeline.created_at.iso8601 },
+ { key: 'CI_COMMIT_SHA',
+ value: job.sha },
+ { key: 'CI_COMMIT_SHORT_SHA',
+ value: job.short_sha },
+ { key: 'CI_COMMIT_BEFORE_SHA',
+ value: job.before_sha },
+ { key: 'CI_COMMIT_REF_NAME',
+ value: job.ref },
+ { key: 'CI_COMMIT_REF_SLUG',
+ value: job.ref_slug },
+ { key: 'CI_COMMIT_BRANCH',
+ value: job.ref },
+ { key: 'CI_COMMIT_MESSAGE',
+ value: pipeline.git_commit_message },
+ { key: 'CI_COMMIT_TITLE',
+ value: pipeline.git_commit_title },
+ { key: 'CI_COMMIT_DESCRIPTION',
+ value: pipeline.git_commit_description },
+ { key: 'CI_COMMIT_REF_PROTECTED',
+ value: (!!pipeline.protected_ref?).to_s },
+ { key: 'CI_COMMIT_TIMESTAMP',
+ value: pipeline.git_commit_timestamp },
+ { key: 'CI_COMMIT_AUTHOR',
+ value: pipeline.git_author_full_text },
+ { key: 'CI_BUILD_REF',
+ value: job.sha },
+ { key: 'CI_BUILD_BEFORE_SHA',
+ value: job.before_sha },
+ { key: 'CI_BUILD_REF_NAME',
+ value: job.ref },
+ { key: 'CI_BUILD_REF_SLUG',
+ value: job.ref_slug },
+ { key: 'YAML_VARIABLE',
+ value: 'value' },
+ { key: 'GITLAB_USER_ID',
+ value: user.id.to_s },
+ { key: 'GITLAB_USER_EMAIL',
+ value: user.email },
+ { key: 'GITLAB_USER_LOGIN',
+ value: user.username },
+ { key: 'GITLAB_USER_NAME',
+ value: user.name }
+ ].map { |var| var.merge(public: true, masked: false) }
+ end
subject { builder.scoped_variables(job, environment: environment, dependencies: dependencies) }
- it 'returns the expected variables' do
- keys = %w[CI_JOB_NAME
- CI_JOB_STAGE
- CI_NODE_TOTAL
- CI_BUILD_NAME
- CI_BUILD_STAGE]
+ it { is_expected.to be_instance_of(Gitlab::Ci::Variables::Collection) }
+
+ it { expect(subject.to_runner_variables).to eq(predefined_variables) }
+
+ context 'variables ordering' do
+ def var(name, value)
+ { key: name, value: value.to_s, public: true, masked: false }
+ end
+
+ before do
+ allow(builder).to receive(:predefined_variables) { [var('A', 1), var('B', 1)] }
+ allow(project).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] }
+ allow(pipeline).to receive(:predefined_variables) { [var('C', 3), var('D', 3)] }
+ allow(job).to receive(:runner) { double(predefined_variables: [var('D', 4), var('E', 4)]) }
+ allow(builder).to receive(:kubernetes_variables) { [var('E', 5), var('F', 5)] }
+ allow(builder).to receive(:deployment_variables) { [var('F', 6), var('G', 6)] }
+ allow(job).to receive(:yaml_variables) { [var('G', 7), var('H', 7)] }
+ allow(builder).to receive(:user_variables) { [var('H', 8), var('I', 8)] }
+ allow(job).to receive(:dependency_variables) { [var('I', 9), var('J', 9)] }
+ allow(builder).to receive(:secret_instance_variables) { [var('J', 10), var('K', 10)] }
+ allow(builder).to receive(:secret_group_variables) { [var('K', 11), var('L', 11)] }
+ allow(builder).to receive(:secret_project_variables) { [var('L', 12), var('M', 12)] }
+ allow(job).to receive(:trigger_request) { double(user_variables: [var('M', 13), var('N', 13)]) }
+ allow(pipeline).to receive(:variables) { [var('N', 14), var('O', 14)] }
+ allow(pipeline).to receive(:pipeline_schedule) { double(job_variables: [var('O', 15), var('P', 15)]) }
+ end
+
+ it 'returns variables in order depending on resource hierarchy' do
+ expect(subject.to_runner_variables).to eq(
+ [var('A', 1), var('B', 1),
+ var('B', 2), var('C', 2),
+ var('C', 3), var('D', 3),
+ var('D', 4), var('E', 4),
+ var('E', 5), var('F', 5),
+ var('F', 6), var('G', 6),
+ var('G', 7), var('H', 7),
+ var('H', 8), var('I', 8),
+ var('I', 9), var('J', 9),
+ var('J', 10), var('K', 10),
+ var('K', 11), var('L', 11),
+ var('L', 12), var('M', 12),
+ var('M', 13), var('N', 13),
+ var('N', 14), var('O', 14),
+ var('O', 15), var('P', 15)])
+ end
- subject.map { |env| env[:key] }.tap do |names|
- expect(names).to include(*keys)
+ it 'overrides duplicate keys depending on resource hierarchy' do
+ expect(subject.to_hash).to match(
+ 'A' => '1', 'B' => '2',
+ 'C' => '3', 'D' => '4',
+ 'E' => '5', 'F' => '6',
+ 'G' => '7', 'H' => '8',
+ 'I' => '9', 'J' => '10',
+ 'K' => '11', 'L' => '12',
+ 'M' => '13', 'N' => '14',
+ 'O' => '15', 'P' => '15')
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index e8b38b21ef8..20af84ce648 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -2097,6 +2097,12 @@ module Gitlab
it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in current or prior stages'
end
+ context 'duplicate needs' do
+ let(:needs) { %w(build1 build1) }
+
+ it_behaves_like 'returns errors', 'test1 has duplicate entries in the needs section.'
+ end
+
context 'needs and dependencies that are mismatching' do
let(:needs) { %w(build1) }
let(:dependencies) { %w(build2) }
@@ -2602,7 +2608,7 @@ module Gitlab
end
context 'returns errors if job stage is not a defined stage' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", type: "acceptance" } }) }
it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, .post'
end
@@ -2638,37 +2644,37 @@ module Gitlab
end
context 'returns errors if job artifacts:name is not an a string' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts name should be a string'
end
context 'returns errors if job artifacts:when is not an a predefined value' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be on_success, on_failure or always'
end
context 'returns errors if job artifacts:expire_in is not an a string' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts expire in should be a duration'
end
context 'returns errors if job artifacts:expire_in is not an a valid duration' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts expire in should be a duration'
end
context 'returns errors if job artifacts:untracked is not an array of strings' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts untracked should be a boolean value'
end
context 'returns errors if job artifacts:paths is not an array of strings' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:artifacts paths should be an array of strings'
end
@@ -2692,49 +2698,49 @@ module Gitlab
end
context 'returns errors if job cache:key is not an a string' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) }
it_behaves_like 'returns errors', "jobs:rspec:cache:key should be a hash, a string or a symbol"
end
context 'returns errors if job cache:key:files is not an array of strings' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key:files config should be an array of strings'
end
context 'returns errors if job cache:key:files is an empty array' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key:files config requires at least 1 item'
end
context 'returns errors if job defines only cache:key:prefix' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key config missing required keys: files'
end
context 'returns errors if job cache:key:prefix is not an a string' do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } }) }
it_behaves_like 'returns errors', 'jobs:rspec:cache:key:prefix config should be a string or symbol'
end
context "returns errors if job cache:untracked is not an array of strings" do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) }
it_behaves_like 'returns errors', "jobs:rspec:cache:untracked config should be a boolean value"
end
context "returns errors if job cache:paths is not an array of strings" do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) }
it_behaves_like 'returns errors', "jobs:rspec:cache:paths config should be an array of strings"
end
context "returns errors if job dependencies is not an array of strings" do
- let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) }
+ let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", dependencies: "string" } }) }
it_behaves_like 'returns errors', "jobs:rspec dependencies should be an array of strings"
end
diff --git a/spec/lib/gitlab/color_schemes_spec.rb b/spec/lib/gitlab/color_schemes_spec.rb
index fd9fccc2bf7..feb5648ff2d 100644
--- a/spec/lib/gitlab/color_schemes_spec.rb
+++ b/spec/lib/gitlab/color_schemes_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::ColorSchemes do
describe '.by_id' do
it 'returns a scheme by its ID' do
- expect(described_class.by_id(1).name).to eq 'White'
+ expect(described_class.by_id(1).name).to eq 'Light'
expect(described_class.by_id(4).name).to eq 'Solarized Dark'
end
end
diff --git a/spec/lib/gitlab/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb
index 0153cfbf091..154038f51c7 100644
--- a/spec/lib/gitlab/config/entry/configurable_spec.rb
+++ b/spec/lib/gitlab/config/entry/configurable_spec.rb
@@ -39,7 +39,8 @@ RSpec.describe Gitlab::Config::Entry::Configurable do
entry :object, entry_class,
description: 'test object',
inherit: true,
- reserved: true
+ reserved: true,
+ deprecation: { deprecated: '10.0', warning: '10.1', removed: '11.0', documentation: 'docs.gitlab.com' }
end
end
@@ -52,6 +53,12 @@ RSpec.describe Gitlab::Config::Entry::Configurable do
factory = entry.nodes[:object]
expect(factory).to be_an_instance_of(Gitlab::Config::Entry::Factory)
+ expect(factory.deprecation).to eq(
+ deprecated: '10.0',
+ warning: '10.1',
+ removed: '11.0',
+ documentation: 'docs.gitlab.com'
+ )
expect(factory.description).to eq('test object')
expect(factory.inheritable?).to eq(true)
expect(factory.reserved?).to eq(true)
diff --git a/spec/lib/gitlab/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb
index a00c45169ef..260b5cf0ade 100644
--- a/spec/lib/gitlab/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/config/entry/factory_spec.rb
@@ -115,5 +115,16 @@ RSpec.describe Gitlab::Config::Entry::Factory do
.with('some value', { some: 'hash' })
end
end
+
+ context 'when setting deprecation information' do
+ it 'passes deprecation as a parameter' do
+ entry = factory
+ .value('some value')
+ .with(deprecation: { deprecated: '10.0', warning: '10.1', removed: '11.0', documentation: 'docs' })
+ .create!
+
+ expect(entry.deprecation).to eq({ deprecated: '10.0', warning: '10.1', removed: '11.0', documentation: 'docs' })
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 56e3fc269e6..08d29f7842c 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://cdn.example.com")
expect(directives['font_src']).to eq("'self' https://cdn.example.com")
expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com')
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/mermaid")
end
end
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'does not add CUSTOMER_PORTAL_URL to CSP' do
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/mermaid")
end
end
@@ -123,7 +123,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds CUSTOMER_PORTAL_URL to CSP' do
- expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/rails/letter_opener/ https://customers.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/rails/letter_opener/ https://customers.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/mermaid")
end
end
end
diff --git a/spec/lib/gitlab/data_builder/archive_trace_spec.rb b/spec/lib/gitlab/data_builder/archive_trace_spec.rb
new file mode 100644
index 00000000000..a310b0f0a94
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/archive_trace_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DataBuilder::ArchiveTrace do
+ let_it_be(:build) { create(:ci_build, :trace_artifact) }
+
+ describe '.build' do
+ let(:data) { described_class.build(build) }
+
+ it 'has correct attributes', :aggregate_failures do
+ expect(data[:object_kind]).to eq 'archive_trace'
+ expect(data[:trace_url]).to eq build.job_artifacts_trace.file.url
+ expect(data[:build_id]).to eq build.id
+ expect(data[:pipeline_id]).to eq build.pipeline_id
+ expect(data[:project]).to eq build.project.hook_attrs
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
index 75741c52579..ab8c8a51694 100644
--- a/spec/lib/gitlab/data_builder/deployment_spec.rb
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -37,6 +37,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expect(data[:user_url]).to eq(expected_user_url)
expect(data[:commit_url]).to eq(expected_commit_url)
expect(data[:commit_title]).to eq(commit.title)
+ expect(data[:ref]).to eq(deployment.ref)
end
it 'does not include the deployable URL when there is no deployable' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index 49714cfc4dd..01d61a525e6 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -336,8 +336,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
describe '#smoothed_time_efficiency' do
- let(:migration) { create(:batched_background_migration, interval: 120.seconds) }
- let(:end_time) { Time.zone.now }
+ let_it_be(:migration) { create(:batched_background_migration, interval: 120.seconds) }
+ let_it_be(:end_time) { Time.zone.now }
around do |example|
freeze_time do
@@ -345,7 +345,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
- let(:common_attrs) do
+ let_it_be(:common_attrs) do
{
status: :succeeded,
batched_migration: migration,
@@ -364,13 +364,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
context 'when there are enough jobs' do
- subject { migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) }
+ let_it_be(:number_of_jobs) { 10 }
+ let_it_be(:jobs) { create_list(:batched_background_migration_job, number_of_jobs, **common_attrs.merge(batched_migration: migration)) }
- let!(:jobs) { create_list(:batched_background_migration_job, number_of_jobs, **common_attrs.merge(batched_migration: migration)) }
- let(:number_of_jobs) { 10 }
+ subject { migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) }
before do
- expect(migration).to receive_message_chain(:batched_jobs, :successful_in_execution_order, :reverse_order, :limit).with(no_args).with(no_args).with(number_of_jobs).and_return(jobs)
+ expect(migration).to receive_message_chain(:batched_jobs, :successful_in_execution_order, :reverse_order, :limit, :with_preloads)
+ .and_return(jobs)
end
def mock_efficiencies(*effs)
@@ -411,6 +412,18 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
end
+
+ context 'with preloaded batched migration' do
+ it 'avoids N+1' do
+ create_list(:batched_background_migration_job, 11, **common_attrs.merge(started_at: end_time - 10.seconds))
+
+ control = ActiveRecord::QueryRecorder.new do
+ migration.smoothed_time_efficiency(number_of_jobs: 10)
+ end
+
+ expect { migration.smoothed_time_efficiency(number_of_jobs: 11) }.not_to exceed_query_limit(control)
+ end
+ end
end
describe '#optimize!' do
diff --git a/spec/lib/gitlab/database/background_migration_job_spec.rb b/spec/lib/gitlab/database/background_migration_job_spec.rb
index 42695925a1c..1117c17c84a 100644
--- a/spec/lib/gitlab/database/background_migration_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration_job_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigrationJob do
it_behaves_like 'having unique enum values'
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
describe '.for_migration_execution' do
let!(:job1) { create(:background_migration_job) }
let!(:job2) { create(:background_migration_job, arguments: ['hi', 2]) }
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index 9831510f014..028bdce852e 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -270,8 +270,6 @@ RSpec.describe Gitlab::Database::BatchCount do
end
it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE}" do
- stub_feature_flags(loose_index_scan_for_distinct_values: false)
-
min_id = model.minimum(:id)
relation = instance_double(ActiveRecord::Relation)
allow(model).to receive_message_chain(:select, public_send: relation)
@@ -317,85 +315,13 @@ RSpec.describe Gitlab::Database::BatchCount do
end
end
- context 'when the loose_index_scan_for_distinct_values feature flag is off' do
- it_behaves_like 'when batch fetch query is canceled' do
- let(:mode) { :distinct }
- let(:operation) { :count }
- let(:operation_args) { nil }
- let(:column) { nil }
-
- subject { described_class.method(:batch_distinct_count) }
-
- before do
- stub_feature_flags(loose_index_scan_for_distinct_values: false)
- end
- end
- end
-
- context 'when the loose_index_scan_for_distinct_values feature flag is on' do
+ it_behaves_like 'when batch fetch query is canceled' do
let(:mode) { :distinct }
let(:operation) { :count }
let(:operation_args) { nil }
let(:column) { nil }
- let(:batch_size) { 10_000 }
-
subject { described_class.method(:batch_distinct_count) }
-
- before do
- stub_feature_flags(loose_index_scan_for_distinct_values: true)
- end
-
- it 'reduces batch size by half and retry fetch' do
- too_big_batch_relation_mock = instance_double(ActiveRecord::Relation)
-
- count_method = double(send: 1)
-
- allow(too_big_batch_relation_mock).to receive(:send).and_raise(ActiveRecord::QueryCanceled)
- allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).with(from: 0, to: batch_size).and_return(too_big_batch_relation_mock)
- allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).with(from: 0, to: batch_size / 2).and_return(count_method)
- allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).with(from: batch_size / 2, to: batch_size).and_return(count_method)
-
- subject.call(model, column, batch_size: batch_size, start: 0, finish: batch_size - 1)
- end
-
- context 'when all retries fail' do
- let(:batch_count_query) { 'SELECT COUNT(id) FROM relation WHERE id BETWEEN 0 and 1' }
-
- before do
- relation = instance_double(ActiveRecord::Relation)
- allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive_message_chain(:new, :build_query).and_return(relation)
- allow(relation).to receive(:send).and_raise(ActiveRecord::QueryCanceled.new('query timed out'))
- allow(relation).to receive(:to_sql).and_return(batch_count_query)
- end
-
- it 'logs failing query' do
- expect(Gitlab::AppJsonLogger).to receive(:error).with(
- event: 'batch_count',
- relation: model.table_name,
- operation: operation,
- operation_args: operation_args,
- start: 0,
- mode: mode,
- query: batch_count_query,
- message: 'Query has been canceled with message: query timed out'
- )
- expect(subject.call(model, column, batch_size: batch_size, start: 0)).to eq(-1)
- end
- end
-
- context 'when LooseIndexScanDistinctCount raises error' do
- let(:column) { :creator_id }
- let(:error_class) { Gitlab::Database::LooseIndexScanDistinctCount::ColumnConfigurationError }
-
- it 'rescues ColumnConfigurationError' do
- allow(Gitlab::Database::LooseIndexScanDistinctCount).to receive(:new).and_raise(error_class.new('error message'))
-
- expect(Gitlab::AppJsonLogger).to receive(:error).with(a_hash_including(message: 'LooseIndexScanDistinctCount column error: error message'))
-
- expect(subject.call(Project, column, batch_size: 10_000, start: 0)).to eq(-1)
- end
- end
end
end
diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb
index 9a6463c99fa..08b4d50f83b 100644
--- a/spec/lib/gitlab/database/bulk_update_spec.rb
+++ b/spec/lib/gitlab/database/bulk_update_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Gitlab::Database::BulkUpdate do
before do
configuration_hash = ActiveRecord::Base.connection_db_config.configuration_hash
- ActiveRecord::Base.establish_connection(
+ ActiveRecord::Base.establish_connection( # rubocop: disable Database/EstablishConnection
configuration_hash.merge(prepared_statements: prepared_statements)
)
end
diff --git a/spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb b/spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb
deleted file mode 100644
index e0eac26e4d9..00000000000
--- a/spec/lib/gitlab/database/loose_index_scan_distinct_count_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::LooseIndexScanDistinctCount do
- context 'counting distinct users' do
- let_it_be(:user) { create(:user) }
- let_it_be(:other_user) { create(:user) }
-
- let(:column) { :creator_id }
-
- before_all do
- create_list(:project, 3, creator: user)
- create_list(:project, 1, creator: other_user)
- end
-
- subject(:count) { described_class.new(Project, :creator_id).count(from: Project.minimum(:creator_id), to: Project.maximum(:creator_id) + 1) }
-
- it { is_expected.to eq(2) }
-
- context 'when STI model is queried' do
- it 'does not raise error' do
- expect { described_class.new(Group, :owner_id).count(from: 0, to: 1) }.not_to raise_error
- end
- end
-
- context 'when model with default_scope is queried' do
- it 'does not raise error' do
- expect { described_class.new(GroupMember, :id).count(from: 0, to: 1) }.not_to raise_error
- end
- end
-
- context 'when the fully qualified column is given' do
- let(:column) { 'projects.creator_id' }
-
- it { is_expected.to eq(2) }
- end
-
- context 'when AR attribute is given' do
- let(:column) { Project.arel_table[:creator_id] }
-
- it { is_expected.to eq(2) }
- end
-
- context 'when invalid value is given for the column' do
- let(:column) { Class.new }
-
- it { expect { described_class.new(Group, column) }.to raise_error(Gitlab::Database::LooseIndexScanDistinctCount::ColumnConfigurationError) }
- end
-
- context 'when null values are present' do
- before do
- create_list(:project, 2).each { |p| p.update_column(:creator_id, nil) }
- end
-
- it { is_expected.to eq(2) }
- end
- end
-
- context 'counting STI models' do
- let!(:groups) { create_list(:group, 3) }
- let!(:namespaces) { create_list(:namespace, 2) }
-
- let(:max_id) { Namespace.maximum(:id) + 1 }
-
- it 'counts groups' do
- count = described_class.new(Group, :id).count(from: 0, to: max_id)
- expect(count).to eq(3)
- end
- end
-end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 7f80bed04a4..7e3de32b965 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1752,116 +1752,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#change_column_type_using_background_migration' do
- let!(:issue) { create(:issue, :closed, closed_at: Time.zone.now) }
-
- let(:issue_model) do
- Class.new(ActiveRecord::Base) do
- self.table_name = 'issues'
- include EachBatch
- end
- end
-
- it 'changes the type of a column using a background migration' do
- expect(model)
- .to receive(:add_column)
- .with('issues', 'closed_at_for_type_change', :datetime_with_timezone)
-
- expect(model)
- .to receive(:install_rename_triggers)
- .with('issues', :closed_at, 'closed_at_for_type_change')
-
- expect(BackgroundMigrationWorker)
- .to receive(:perform_in)
- .ordered
- .with(
- 10.minutes,
- 'CopyColumn',
- ['issues', :closed_at, 'closed_at_for_type_change', issue.id, issue.id]
- )
-
- expect(BackgroundMigrationWorker)
- .to receive(:perform_in)
- .ordered
- .with(
- 1.hour + 10.minutes,
- 'CleanupConcurrentTypeChange',
- ['issues', :closed_at, 'closed_at_for_type_change']
- )
-
- expect(Gitlab::BackgroundMigration)
- .to receive(:steal)
- .ordered
- .with('CopyColumn')
-
- expect(Gitlab::BackgroundMigration)
- .to receive(:steal)
- .ordered
- .with('CleanupConcurrentTypeChange')
-
- model.change_column_type_using_background_migration(
- issue_model.all,
- :closed_at,
- :datetime_with_timezone
- )
- end
- end
-
- describe '#rename_column_using_background_migration' do
- let!(:issue) { create(:issue, :closed, closed_at: Time.zone.now) }
-
- it 'renames a column using a background migration' do
- expect(model)
- .to receive(:add_column)
- .with(
- 'issues',
- :closed_at_timestamp,
- :datetime_with_timezone,
- limit: anything,
- precision: anything,
- scale: anything
- )
-
- expect(model)
- .to receive(:install_rename_triggers)
- .with('issues', :closed_at, :closed_at_timestamp)
-
- expect(BackgroundMigrationWorker)
- .to receive(:perform_in)
- .ordered
- .with(
- 10.minutes,
- 'CopyColumn',
- ['issues', :closed_at, :closed_at_timestamp, issue.id, issue.id]
- )
-
- expect(BackgroundMigrationWorker)
- .to receive(:perform_in)
- .ordered
- .with(
- 1.hour + 10.minutes,
- 'CleanupConcurrentRename',
- ['issues', :closed_at, :closed_at_timestamp]
- )
-
- expect(Gitlab::BackgroundMigration)
- .to receive(:steal)
- .ordered
- .with('CopyColumn')
-
- expect(Gitlab::BackgroundMigration)
- .to receive(:steal)
- .ordered
- .with('CleanupConcurrentRename')
-
- model.rename_column_using_background_migration(
- 'issues',
- :closed_at,
- :closed_at_timestamp
- )
- end
- end
-
describe '#convert_to_bigint_column' do
it 'returns the name of the temporary column used to convert to bigint' do
expect(model.convert_to_bigint_column(:id)).to eq('id_convert_to_bigint')
@@ -2065,8 +1955,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
t.integer :other_id
t.timestamps
end
-
- allow(model).to receive(:perform_background_migration_inline?).and_return(false)
end
context 'when the target table does not exist' do
diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
index 99c7d70724c..0abb76b9f8a 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -7,249 +7,208 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
ActiveRecord::Migration.new.extend(described_class)
end
- describe '#queue_background_migration_jobs_by_range_at_intervals' do
- context 'when the model has an ID column' do
- let!(:id1) { create(:user).id }
- let!(:id2) { create(:user).id }
- let!(:id3) { create(:user).id }
-
- around do |example|
- freeze_time { example.run }
- end
-
- before do
- User.class_eval do
- include EachBatch
- end
- end
+ shared_examples_for 'helpers that enqueue background migrations' do |worker_class, tracking_database|
+ before do
+ allow(model).to receive(:tracking_database).and_return(tracking_database)
+ end
- it 'returns the final expected delay' do
- Sidekiq::Testing.fake! do
- final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
+ describe '#queue_background_migration_jobs_by_range_at_intervals' do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
- expect(final_delay.to_f).to eq(20.minutes.to_f)
+ around do |example|
+ freeze_time { example.run }
end
- end
-
- it 'returns zero when nothing gets queued' do
- Sidekiq::Testing.fake! do
- final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User.none, 'FooJob', 10.minutes)
- expect(final_delay).to eq(0)
+ before do
+ User.class_eval do
+ include EachBatch
+ end
end
- end
- context 'with batch_size option' do
- it 'queues jobs correctly' do
+ it 'returns the final expected delay' do
Sidekiq::Testing.fake! do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f)
+ expect(final_delay.to_f).to eq(20.minutes.to_f)
end
end
- end
- context 'without batch_size option' do
- it 'queues jobs correctly' do
+ it 'returns zero when nothing gets queued' do
Sidekiq::Testing.fake! do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes)
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User.none, 'FooJob', 10.minutes)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ expect(final_delay).to eq(0)
end
end
- end
- context 'with other_job_arguments option' do
- it 'queues jobs correctly' do
- Sidekiq::Testing.fake! do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2])
+ context 'when the delay_interval is smaller than the minimum' do
+ it 'sets the delay_interval to the minimum value' do
+ Sidekiq::Testing.fake! do
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 1.minute, batch_size: 2)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ expect(worker_class.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(worker_class.jobs[0]['at']).to eq(2.minutes.from_now.to_f)
+ expect(worker_class.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(worker_class.jobs[1]['at']).to eq(4.minutes.from_now.to_f)
+
+ expect(final_delay.to_f).to eq(4.minutes.to_f)
+ end
end
end
- end
- context 'with initial_delay option' do
- it 'queues jobs correctly' do
- Sidekiq::Testing.fake! do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2], initial_delay: 10.minutes)
+ context 'with batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(20.minutes.from_now.to_f)
+ expect(worker_class.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(worker_class.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ expect(worker_class.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(worker_class.jobs[1]['at']).to eq(20.minutes.from_now.to_f)
+ end
end
end
- end
-
- context 'with track_jobs option' do
- it 'creates a record for each job in the database' do
- Sidekiq::Testing.fake! do
- expect do
- model.queue_background_migration_jobs_by_range_at_intervals(User, '::FooJob', 10.minutes,
- other_job_arguments: [1, 2], track_jobs: true)
- end.to change { Gitlab::Database::BackgroundMigrationJob.count }.from(0).to(1)
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- tracked_job = Gitlab::Database::BackgroundMigrationJob.first
+ context 'without batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes)
- expect(tracked_job.class_name).to eq('FooJob')
- expect(tracked_job.arguments).to eq([id1, id3, 1, 2])
- expect(tracked_job).to be_pending
+ expect(worker_class.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ expect(worker_class.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ end
end
end
- end
- context 'without track_jobs option' do
- it 'does not create records in the database' do
- Sidekiq::Testing.fake! do
- expect do
+ context 'with other_job_arguments option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2])
- end.not_to change { Gitlab::Database::BackgroundMigrationJob.count }
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ expect(worker_class.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
+ expect(worker_class.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ end
end
end
- end
- end
-
- context 'when the model specifies a primary_column_name' do
- let!(:id1) { create(:container_expiration_policy).id }
- let!(:id2) { create(:container_expiration_policy).id }
- let!(:id3) { create(:container_expiration_policy).id }
- around do |example|
- freeze_time { example.run }
- end
+ context 'with initial_delay option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2], initial_delay: 10.minutes)
- before do
- ContainerExpirationPolicy.class_eval do
- include EachBatch
+ expect(worker_class.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
+ expect(worker_class.jobs[0]['at']).to eq(20.minutes.from_now.to_f)
+ end
+ end
end
- end
- it 'returns the final expected delay', :aggregate_failures do
- Sidekiq::Testing.fake! do
- final_delay = model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, batch_size: 2, primary_column_name: :project_id)
+ context 'with track_jobs option' do
+ it 'creates a record for each job in the database' do
+ Sidekiq::Testing.fake! do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, '::FooJob', 10.minutes,
+ other_job_arguments: [1, 2], track_jobs: true)
+ end.to change { Gitlab::Database::BackgroundMigrationJob.count }.from(0).to(1)
- expect(final_delay.to_f).to eq(20.minutes.to_f)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f)
- end
- end
+ expect(worker_class.jobs.size).to eq(1)
- context "when the primary_column_name is not an integer" do
- it 'raises error' do
- expect do
- model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :enabled)
- end.to raise_error(StandardError, /is not an integer column/)
- end
- end
+ tracked_job = Gitlab::Database::BackgroundMigrationJob.first
- context "when the primary_column_name does not exist" do
- it 'raises error' do
- expect do
- model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :foo)
- end.to raise_error(StandardError, /does not have an ID column of foo/)
+ expect(tracked_job.class_name).to eq('FooJob')
+ expect(tracked_job.arguments).to eq([id1, id3, 1, 2])
+ expect(tracked_job).to be_pending
+ end
+ end
end
- end
- end
-
- context "when the model doesn't have an ID or primary_column_name column" do
- it 'raises error (for now)' do
- expect do
- model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds)
- end.to raise_error(StandardError, /does not have an ID/)
- end
- end
- end
- describe '#requeue_background_migration_jobs_by_range_at_intervals' do
- let!(:job_class_name) { 'TestJob' }
- let!(:pending_job_1) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1, 2]) }
- let!(:pending_job_2) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [3, 4]) }
- let!(:successful_job_1) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [5, 6]) }
- let!(:successful_job_2) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [7, 8]) }
+ context 'without track_jobs option' do
+ it 'does not create records in the database' do
+ Sidekiq::Testing.fake! do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2])
+ end.not_to change { Gitlab::Database::BackgroundMigrationJob.count }
- around do |example|
- freeze_time do
- Sidekiq::Testing.fake! do
- example.run
+ expect(worker_class.jobs.size).to eq(1)
+ end
+ end
end
end
- end
-
- subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes) }
-
- it 'returns the expected duration' do
- expect(subject).to eq(20.minutes)
- end
- context 'when nothing is queued' do
- subject { model.requeue_background_migration_jobs_by_range_at_intervals('FakeJob', 10.minutes) }
+ context 'when the model specifies a primary_column_name' do
+ let!(:id1) { create(:container_expiration_policy).id }
+ let!(:id2) { create(:container_expiration_policy).id }
+ let!(:id3) { create(:container_expiration_policy).id }
- it 'returns expected duration of zero when nothing gets queued' do
- expect(subject).to eq(0)
- end
- end
-
- it 'queues pending jobs' do
- subject
+ around do |example|
+ freeze_time { example.run }
+ end
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([job_class_name, [1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([job_class_name, [3, 4]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
- end
+ before do
+ ContainerExpirationPolicy.class_eval do
+ include EachBatch
+ end
+ end
- context 'with batch_size option' do
- subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes, batch_size: 1) }
+ it 'returns the final expected delay', :aggregate_failures do
+ Sidekiq::Testing.fake! do
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, batch_size: 2, primary_column_name: :project_id)
- it 'returns the expected duration' do
- expect(subject).to eq(20.minutes)
- end
+ expect(final_delay.to_f).to eq(20.minutes.to_f)
+ expect(worker_class.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(worker_class.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ expect(worker_class.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(worker_class.jobs[1]['at']).to eq(20.minutes.from_now.to_f)
+ end
+ end
- it 'queues pending jobs' do
- subject
+ context "when the primary_column_name is not an integer" do
+ it 'raises error' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :enabled)
+ end.to raise_error(StandardError, /is not an integer column/)
+ end
+ end
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([job_class_name, [1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([job_class_name, [3, 4]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
+ context "when the primary_column_name does not exist" do
+ it 'raises error' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :foo)
+ end.to raise_error(StandardError, /does not have an ID column of foo/)
+ end
+ end
end
- it 'retrieve jobs in batches' do
- jobs = double('jobs')
- expect(Gitlab::Database::BackgroundMigrationJob).to receive(:pending) { jobs }
- allow(jobs).to receive(:where).with(class_name: job_class_name) { jobs }
- expect(jobs).to receive(:each_batch).with(of: 1)
-
- subject
+ context "when the model doesn't have an ID or primary_column_name column" do
+ it 'raises error (for now)' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds)
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
end
end
- context 'with initial_delay option' do
- let_it_be(:initial_delay) { 3.minutes }
+ describe '#requeue_background_migration_jobs_by_range_at_intervals' do
+ let!(:job_class_name) { 'TestJob' }
+ let!(:pending_job_1) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1, 2]) }
+ let!(:pending_job_2) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [3, 4]) }
+ let!(:successful_job_1) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [5, 6]) }
+ let!(:successful_job_2) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [7, 8]) }
- subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes, initial_delay: initial_delay) }
-
- it 'returns the expected duration' do
- expect(subject).to eq(23.minutes)
+ around do |example|
+ freeze_time do
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
end
- it 'queues pending jobs' do
- subject
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes) }
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([job_class_name, [1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(3.minutes.from_now.to_f)
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([job_class_name, [3, 4]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(13.minutes.from_now.to_f)
+ it 'returns the expected duration' do
+ expect(subject).to eq(20.minutes)
end
context 'when nothing is queued' do
@@ -259,195 +218,226 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
expect(subject).to eq(0)
end
end
- end
- end
- describe '#perform_background_migration_inline?' do
- it 'returns true in a test environment' do
- stub_rails_env('test')
+ it 'queues pending jobs' do
+ subject
- expect(model.perform_background_migration_inline?).to eq(true)
- end
+ expect(worker_class.jobs[0]['args']).to eq([job_class_name, [1, 2]])
+ expect(worker_class.jobs[0]['at']).to be_nil
+ expect(worker_class.jobs[1]['args']).to eq([job_class_name, [3, 4]])
+ expect(worker_class.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
+ end
- it 'returns true in a development environment' do
- stub_rails_env('development')
+ context 'with batch_size option' do
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes, batch_size: 1) }
- expect(model.perform_background_migration_inline?).to eq(true)
- end
+ it 'returns the expected duration' do
+ expect(subject).to eq(20.minutes)
+ end
- it 'returns false in a production environment' do
- stub_rails_env('production')
+ it 'queues pending jobs' do
+ subject
- expect(model.perform_background_migration_inline?).to eq(false)
- end
- end
+ expect(worker_class.jobs[0]['args']).to eq([job_class_name, [1, 2]])
+ expect(worker_class.jobs[0]['at']).to be_nil
+ expect(worker_class.jobs[1]['args']).to eq([job_class_name, [3, 4]])
+ expect(worker_class.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
+ end
- describe '#migrate_async' do
- it 'calls BackgroundMigrationWorker.perform_async' do
- expect(BackgroundMigrationWorker).to receive(:perform_async).with("Class", "hello", "world")
+ it 'retrieve jobs in batches' do
+ jobs = double('jobs')
+ expect(Gitlab::Database::BackgroundMigrationJob).to receive(:pending) { jobs }
+ allow(jobs).to receive(:where).with(class_name: job_class_name) { jobs }
+ expect(jobs).to receive(:each_batch).with(of: 1)
- model.migrate_async("Class", "hello", "world")
- end
+ subject
+ end
+ end
- it 'pushes a context with the current class name as caller_id' do
- expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+ context 'with initial_delay option' do
+ let_it_be(:initial_delay) { 3.minutes }
- model.migrate_async('Class', 'hello', 'world')
- end
- end
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes, initial_delay: initial_delay) }
- describe '#migrate_in' do
- it 'calls BackgroundMigrationWorker.perform_in' do
- expect(BackgroundMigrationWorker).to receive(:perform_in).with(10.minutes, 'Class', 'Hello', 'World')
+ it 'returns the expected duration' do
+ expect(subject).to eq(23.minutes)
+ end
- model.migrate_in(10.minutes, 'Class', 'Hello', 'World')
- end
+ it 'queues pending jobs' do
+ subject
+
+ expect(worker_class.jobs[0]['args']).to eq([job_class_name, [1, 2]])
+ expect(worker_class.jobs[0]['at']).to eq(3.minutes.from_now.to_f)
+ expect(worker_class.jobs[1]['args']).to eq([job_class_name, [3, 4]])
+ expect(worker_class.jobs[1]['at']).to eq(13.minutes.from_now.to_f)
+ end
- it 'pushes a context with the current class name as caller_id' do
- expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+ context 'when nothing is queued' do
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals('FakeJob', 10.minutes) }
- model.migrate_in(10.minutes, 'Class', 'Hello', 'World')
+ it 'returns expected duration of zero when nothing gets queued' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
end
- end
- describe '#bulk_migrate_async' do
- it 'calls BackgroundMigrationWorker.bulk_perform_async' do
- expect(BackgroundMigrationWorker).to receive(:bulk_perform_async).with([%w(Class hello world)])
+ describe '#finalized_background_migration' do
+ let(:coordinator) { Gitlab::BackgroundMigration::JobCoordinator.new(worker_class) }
- model.bulk_migrate_async([%w(Class hello world)])
- end
+ let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
+ let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
+ let!(:job_class_name) { 'TestJob' }
- it 'pushes a context with the current class name as caller_id' do
- expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+ let!(:job_class) do
+ Class.new do
+ def perform(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('TestJob', arguments)
+ end
+ end
+ end
- model.bulk_migrate_async([%w(Class hello world)])
- end
- end
+ before do
+ allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
+ .with('main').and_return(coordinator)
- describe '#bulk_migrate_in' do
- it 'calls BackgroundMigrationWorker.bulk_perform_in_' do
- expect(BackgroundMigrationWorker).to receive(:bulk_perform_in).with(10.minutes, [%w(Class hello world)])
+ expect(coordinator).to receive(:migration_class_for)
+ .with(job_class_name).at_least(:once) { job_class }
- model.bulk_migrate_in(10.minutes, [%w(Class hello world)])
- end
+ Sidekiq::Testing.disable! do
+ worker_class.perform_async(job_class_name, [1, 2])
+ worker_class.perform_async(job_class_name, [3, 4])
+ worker_class.perform_in(10, job_class_name, [5, 6])
+ worker_class.perform_in(20, job_class_name, [7, 8])
+ end
+ end
- it 'pushes a context with the current class name as caller_id' do
- expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
+ it_behaves_like 'finalized tracked background migration', worker_class do
+ before do
+ model.finalize_background_migration(job_class_name)
+ end
+ end
- model.bulk_migrate_in(10.minutes, [%w(Class hello world)])
- end
- end
+ context 'when removing all tracked job records' do
+ let!(:job_class) do
+ Class.new do
+ def perform(*arguments)
+ # Force pending jobs to remain pending
+ end
+ end
+ end
- describe '#delete_queued_jobs' do
- let(:job1) { double }
- let(:job2) { double }
+ before do
+ model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded])
+ end
- it 'deletes all queued jobs for the given background migration' do
- expect(Gitlab::BackgroundMigration).to receive(:steal).with('BackgroundMigrationClassName') do |&block|
- expect(block.call(job1)).to be(false)
- expect(block.call(job2)).to be(false)
+ it_behaves_like 'finalized tracked background migration', worker_class
+ it_behaves_like 'removed tracked jobs', 'pending'
+ it_behaves_like 'removed tracked jobs', 'succeeded'
end
- expect(job1).to receive(:delete)
- expect(job2).to receive(:delete)
+ context 'when retaining all tracked job records' do
+ before do
+ model.finalize_background_migration(job_class_name, delete_tracking_jobs: false)
+ end
- model.delete_queued_jobs('BackgroundMigrationClassName')
- end
- end
+ it_behaves_like 'finalized background migration', worker_class
+ include_examples 'retained tracked jobs', 'succeeded'
+ end
- describe '#finalized_background_migration' do
- let(:job_coordinator) { Gitlab::BackgroundMigration::JobCoordinator.new(BackgroundMigrationWorker) }
+ context 'during retry race condition' do
+ let!(:job_class) do
+ Class.new do
+ class << self
+ attr_accessor :worker_class
- let!(:job_class_name) { 'TestJob' }
- let!(:job_class) { Class.new }
- let!(:job_perform_method) do
- ->(*arguments) do
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- # Value is 'TestJob' defined by :job_class_name in the let! above.
- # Scoping prohibits us from directly referencing job_class_name.
- RSpec.current_example.example_group_instance.job_class_name,
- arguments
- )
- end
- end
+ def queue_items_added
+ @queue_items_added ||= []
+ end
+ end
- let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
- let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
+ def worker_class
+ self.class.worker_class
+ end
- before do
- job_class.define_method(:perform, job_perform_method)
+ def queue_items_added
+ self.class.queue_items_added
+ end
- allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
- .with('main').and_return(job_coordinator)
+ def perform(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('TestJob', arguments)
- expect(job_coordinator).to receive(:migration_class_for)
- .with(job_class_name).at_least(:once) { job_class }
+ # Mock another process pushing queue jobs.
+ if self.class.queue_items_added.count < 10
+ Sidekiq::Testing.disable! do
+ queue_items_added << worker_class.perform_async('TestJob', [Time.current])
+ queue_items_added << worker_class.perform_in(10, 'TestJob', [Time.current])
+ end
+ end
+ end
+ end
+ end
- Sidekiq::Testing.disable! do
- BackgroundMigrationWorker.perform_async(job_class_name, [1, 2])
- BackgroundMigrationWorker.perform_async(job_class_name, [3, 4])
- BackgroundMigrationWorker.perform_in(10, job_class_name, [5, 6])
- BackgroundMigrationWorker.perform_in(20, job_class_name, [7, 8])
- end
- end
+ it_behaves_like 'finalized tracked background migration', worker_class do
+ before do
+ # deliberately set the worker class on our test job since it won't be pulled from the surrounding scope
+ job_class.worker_class = worker_class
- it_behaves_like 'finalized tracked background migration' do
- before do
- model.finalize_background_migration(job_class_name)
+ model.finalize_background_migration(job_class_name, delete_tracking_jobs: ['succeeded'])
+ end
+ end
end
end
- context 'when removing all tracked job records' do
- # Force pending jobs to remain pending.
- let!(:job_perform_method) { ->(*arguments) { } }
+ describe '#migrate_in' do
+ it 'calls perform_in for the correct worker' do
+ expect(worker_class).to receive(:perform_in).with(10.minutes, 'Class', 'Hello', 'World')
- before do
- model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded])
+ model.migrate_in(10.minutes, 'Class', 'Hello', 'World')
end
- it_behaves_like 'finalized tracked background migration'
- it_behaves_like 'removed tracked jobs', 'pending'
- it_behaves_like 'removed tracked jobs', 'succeeded'
- end
+ it 'pushes a context with the current class name as caller_id' do
+ expect(Gitlab::ApplicationContext).to receive(:with_context).with(caller_id: model.class.to_s)
- context 'when retaining all tracked job records' do
- before do
- model.finalize_background_migration(job_class_name, delete_tracking_jobs: false)
+ model.migrate_in(10.minutes, 'Class', 'Hello', 'World')
end
- it_behaves_like 'finalized background migration'
- include_examples 'retained tracked jobs', 'succeeded'
- end
+ context 'when a specific coordinator is given' do
+ let(:coordinator) { Gitlab::BackgroundMigration::JobCoordinator.for_tracking_database('main') }
- context 'during retry race condition' do
- let(:queue_items_added) { [] }
- let!(:job_perform_method) do
- ->(*arguments) do
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- RSpec.current_example.example_group_instance.job_class_name,
- arguments
- )
-
- # Mock another process pushing queue jobs.
- queue_items_added = RSpec.current_example.example_group_instance.queue_items_added
- if queue_items_added.count < 10
- Sidekiq::Testing.disable! do
- job_class_name = RSpec.current_example.example_group_instance.job_class_name
- queue_items_added << BackgroundMigrationWorker.perform_async(job_class_name, [Time.current])
- queue_items_added << BackgroundMigrationWorker.perform_in(10, job_class_name, [Time.current])
- end
- end
+ it 'uses that coordinator' do
+ expect(coordinator).to receive(:perform_in).with(10.minutes, 'Class', 'Hello', 'World').and_call_original
+ expect(worker_class).to receive(:perform_in).with(10.minutes, 'Class', 'Hello', 'World')
+
+ model.migrate_in(10.minutes, 'Class', 'Hello', 'World', coordinator: coordinator)
end
end
+ end
- it_behaves_like 'finalized tracked background migration' do
- before do
- model.finalize_background_migration(job_class_name, delete_tracking_jobs: ['succeeded'])
+ describe '#delete_queued_jobs' do
+ let(:job1) { double }
+ let(:job2) { double }
+
+ it 'deletes all queued jobs for the given background migration' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
+ expect(coordinator).to receive(:steal).with('BackgroundMigrationClassName') do |&block|
+ expect(block.call(job1)).to be(false)
+ expect(block.call(job2)).to be(false)
+ end
end
+
+ expect(job1).to receive(:delete)
+ expect(job2).to receive(:delete)
+
+ model.delete_queued_jobs('BackgroundMigrationClassName')
end
end
end
+ context 'when the migration is running against the main database' do
+ it_behaves_like 'helpers that enqueue background migrations', BackgroundMigrationWorker, 'main'
+ end
+
describe '#delete_job_tracking' do
let!(:job_class_name) { 'TestJob' }
diff --git a/spec/lib/gitlab/database/migrations/runner_spec.rb b/spec/lib/gitlab/database/migrations/runner_spec.rb
index 4616bd6941e..7dc965c84fa 100644
--- a/spec/lib/gitlab/database/migrations/runner_spec.rb
+++ b/spec/lib/gitlab/database/migrations/runner_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Database::Migrations::Runner do
allow(ActiveRecord::Migrator).to receive(:new) do |dir, _all_migrations, _schema_migration_class, version_to_migrate|
migrator = double(ActiveRecord::Migrator)
expect(migrator).to receive(:run) do
- migration_runs << OpenStruct.new(dir: dir, version_to_migrate: version_to_migrate)
+ migration_runs << double('migrator', dir: dir, version_to_migrate: version_to_migrate)
end
migrator
end
diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
new file mode 100644
index 00000000000..e5a8143fcc3
--- /dev/null
+++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'cross-database foreign keys' do
+ # TODO: We are trying to empty out this list in
+ # https://gitlab.com/groups/gitlab-org/-/epics/7249 . Once we are done we can
+ # keep this test and assert that there are no cross-db foreign keys. We
+ # should not be adding anything to this list but should instead only add new
+ # loose foreign keys
+ # https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html .
+ let(:allowed_cross_database_foreign_keys) do
+ %w(
+ ci_build_report_results.project_id
+ ci_builds.project_id
+ ci_builds_metadata.project_id
+ ci_daily_build_group_report_results.group_id
+ ci_daily_build_group_report_results.project_id
+ ci_freeze_periods.project_id
+ ci_job_artifacts.project_id
+ ci_job_token_project_scope_links.added_by_id
+ ci_job_token_project_scope_links.source_project_id
+ ci_job_token_project_scope_links.target_project_id
+ ci_pending_builds.namespace_id
+ ci_pending_builds.project_id
+ ci_pipeline_schedules.owner_id
+ ci_pipeline_schedules.project_id
+ ci_pipelines.merge_request_id
+ ci_pipelines.project_id
+ ci_project_monthly_usages.project_id
+ ci_refs.project_id
+ ci_resource_groups.project_id
+ ci_runner_namespaces.namespace_id
+ ci_runner_projects.project_id
+ ci_running_builds.project_id
+ ci_sources_pipelines.project_id
+ ci_sources_pipelines.source_project_id
+ ci_sources_projects.source_project_id
+ ci_stages.project_id
+ ci_subscriptions_projects.downstream_project_id
+ ci_subscriptions_projects.upstream_project_id
+ ci_triggers.owner_id
+ ci_triggers.project_id
+ ci_unit_tests.project_id
+ ci_variables.project_id
+ dast_profiles_pipelines.ci_pipeline_id
+ dast_scanner_profiles_builds.ci_build_id
+ dast_site_profiles_builds.ci_build_id
+ dast_site_profiles_pipelines.ci_pipeline_id
+ external_pull_requests.project_id
+ merge_requests.head_pipeline_id
+ merge_trains.pipeline_id
+ requirements_management_test_reports.build_id
+ security_scans.build_id
+ vulnerability_feedback.pipeline_id
+ vulnerability_occurrence_pipelines.pipeline_id
+ vulnerability_statistics.latest_pipeline_id
+ ).freeze
+ end
+
+ def foreign_keys_for(table_name)
+ ApplicationRecord.connection.foreign_keys(table_name)
+ end
+
+ def is_cross_db?(fk_record)
+ Gitlab::Database::GitlabSchema.table_schemas([fk_record.from_table, fk_record.to_table]).many?
+ end
+
+ it 'onlies have allowed list of cross-database foreign keys', :aggregate_failures do
+ all_tables = ApplicationRecord.connection.data_sources
+
+ all_tables.each do |table|
+ foreign_keys_for(table).each do |fk|
+ if is_cross_db?(fk)
+ column = "#{fk.from_table}.#{fk.column}"
+ expect(allowed_cross_database_foreign_keys).to include(column), "Found extra cross-database foreign key #{column} referencing #{fk.to_table} with constraint name #{fk.name}. When a foreign key references another database you must use a Loose Foreign Key instead https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html ."
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index 5e107109fc9..64dcdb9628a 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) }
let(:partitioning_strategy) { double(missing_partitions: partitions, extra_partitions: [], after_adding_partitions: nil) }
let(:connection) { ActiveRecord::Base.connection }
- let(:table) { "some_table" }
+ let(:table) { "issues" }
before do
allow(connection).to receive(:table_exists?).and_call_original
@@ -36,6 +36,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
it 'creates the partition' do
+ expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
expect(connection).to receive(:execute).with(partitions.first.to_sql)
expect(connection).to receive(:execute).with(partitions.second.to_sql)
diff --git a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
index 636a09e5710..1cec0463055 100644
--- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
let(:connection) { ActiveRecord::Base.connection }
let(:table_name) { :_test_partitioned_test }
- let(:model) { double('model', table_name: table_name, ignored_columns: %w[partition]) }
+ let(:model) { double('model', table_name: table_name, ignored_columns: %w[partition], connection: connection) }
let(:next_partition_if) { double('next_partition_if') }
let(:detach_partition_if) { double('detach_partition_if') }
@@ -94,7 +94,8 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
let(:detach_partition_if) { ->(p) { p != 5 } }
it 'is the leading set of partitions before that value' do
- expect(strategy.extra_partitions.map(&:value)).to contain_exactly(1, 2, 3, 4)
+ # should not contain partition 2 since it's the default value for the partition column
+ expect(strategy.extra_partitions.map(&:value)).to contain_exactly(1, 3, 4)
end
end
@@ -102,7 +103,7 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
let(:detach_partition_if) { proc { true } }
it 'is all but the most recent partition', :aggregate_failures do
- expect(strategy.extra_partitions.map(&:value)).to contain_exactly(1, 2, 3, 4, 5, 6, 7, 8, 9)
+ expect(strategy.extra_partitions.map(&:value)).to contain_exactly(1, 3, 4, 5, 6, 7, 8, 9)
expect(strategy.current_partitions.map(&:value).max).to eq(10)
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
index c43b51e10a0..3072c413246 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
@@ -3,14 +3,15 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable, '#perform' do
- subject { described_class.new }
+ subject(:backfill_job) { described_class.new(connection: connection) }
+ let(:connection) { ActiveRecord::Base.connection }
let(:source_table) { '_test_partitioning_backfills' }
let(:destination_table) { "#{source_table}_part" }
let(:unique_key) { 'id' }
before do
- allow(subject).to receive(:transaction_open?).and_return(false)
+ allow(backfill_job).to receive(:transaction_open?).and_return(false)
end
context 'when the destination table exists' do
@@ -50,10 +51,9 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
stub_const("#{described_class}::SUB_BATCH_SIZE", 2)
stub_const("#{described_class}::PAUSE_SECONDS", pause_seconds)
- allow(subject).to receive(:sleep)
+ allow(backfill_job).to receive(:sleep)
end
- let(:connection) { ActiveRecord::Base.connection }
let(:source_model) { Class.new(ActiveRecord::Base) }
let(:destination_model) { Class.new(ActiveRecord::Base) }
let(:timestamp) { Time.utc(2020, 1, 2).round }
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
it 'copies data into the destination table idempotently' do
expect(destination_model.count).to eq(0)
- subject.perform(source1.id, source3.id, source_table, destination_table, unique_key)
+ backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
expect(destination_model.count).to eq(3)
@@ -76,7 +76,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
expect(destination_record.attributes).to eq(source_record.attributes)
end
- subject.perform(source1.id, source3.id, source_table, destination_table, unique_key)
+ backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
expect(destination_model.count).to eq(3)
end
@@ -87,13 +87,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
expect(bulk_copy).to receive(:copy_between).with(source3.id, source3.id)
end
- subject.perform(source1.id, source3.id, source_table, destination_table, unique_key)
+ backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
end
it 'pauses after copying each sub-batch' do
- expect(subject).to receive(:sleep).with(pause_seconds).twice
+ expect(backfill_job).to receive(:sleep).with(pause_seconds).twice
- subject.perform(source1.id, source3.id, source_table, destination_table, unique_key)
+ backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
end
it 'marks each job record as succeeded after processing' do
@@ -103,7 +103,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original
expect do
- subject.perform(source1.id, source3.id, source_table, destination_table, unique_key)
+ backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
end.to change { ::Gitlab::Database::BackgroundMigrationJob.succeeded.count }.from(0).to(1)
end
@@ -111,24 +111,24 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
create(:background_migration_job, class_name: "::#{described_class.name}",
arguments: [source1.id, source3.id, source_table, destination_table, unique_key])
- jobs_updated = subject.perform(source1.id, source3.id, source_table, destination_table, unique_key)
+ jobs_updated = backfill_job.perform(source1.id, source3.id, source_table, destination_table, unique_key)
expect(jobs_updated).to eq(1)
end
context 'when the job is run within an explicit transaction block' do
- let(:mock_connection) { double('connection') }
+ subject(:backfill_job) { described_class.new(connection: mock_connection) }
- before do
- allow(subject).to receive(:connection).and_return(mock_connection)
- allow(subject).to receive(:transaction_open?).and_return(true)
- end
+ let(:mock_connection) { double('connection') }
it 'raises an error before copying data' do
+ expect(backfill_job).to receive(:transaction_open?).and_call_original
+
+ expect(mock_connection).to receive(:transaction_open?).and_return(true)
expect(mock_connection).not_to receive(:execute)
expect do
- subject.perform(1, 100, source_table, destination_table, unique_key)
+ backfill_job.perform(1, 100, source_table, destination_table, unique_key)
end.to raise_error(/Aborting job to backfill partitioned #{source_table}/)
expect(destination_model.count).to eq(0)
@@ -137,24 +137,25 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
end
context 'when the destination table does not exist' do
+ subject(:backfill_job) { described_class.new(connection: mock_connection) }
+
let(:mock_connection) { double('connection') }
let(:mock_logger) { double('logger') }
before do
- allow(subject).to receive(:connection).and_return(mock_connection)
- allow(subject).to receive(:logger).and_return(mock_logger)
-
- expect(mock_connection).to receive(:table_exists?).with(destination_table).and_return(false)
+ allow(backfill_job).to receive(:logger).and_return(mock_logger)
allow(mock_logger).to receive(:warn)
end
it 'exits without attempting to copy data' do
+ expect(mock_connection).to receive(:table_exists?).with(destination_table).and_return(false)
expect(mock_connection).not_to receive(:execute)
subject.perform(1, 100, source_table, destination_table, unique_key)
end
it 'logs a warning message that the job was skipped' do
+ expect(mock_connection).to receive(:table_exists?).with(destination_table).and_return(false)
expect(mock_logger).to receive(:warn).with(/#{destination_table} does not exist/)
subject.perform(1, 100, source_table, destination_table, unique_key)
diff --git a/spec/lib/gitlab/database/reflection_spec.rb b/spec/lib/gitlab/database/reflection_spec.rb
index 7c3d797817d..efc5bd1c1e1 100644
--- a/spec/lib/gitlab/database/reflection_spec.rb
+++ b/spec/lib/gitlab/database/reflection_spec.rb
@@ -259,6 +259,66 @@ RSpec.describe Gitlab::Database::Reflection do
end
end
+ describe '#flavor', :delete do
+ let(:result) { [double] }
+ let(:connection) { database.model.connection }
+
+ def stub_statements(statements)
+ statements = Array.wrap(statements)
+ execute = connection.method(:execute)
+
+ allow(connection).to receive(:execute) do |arg|
+ if statements.include?(arg)
+ result
+ else
+ execute.call(arg)
+ end
+ end
+ end
+
+ it 're-raises exceptions not matching expected messages' do
+ expect(database.model.connection)
+ .to receive(:execute)
+ .and_raise(ActiveRecord::StatementInvalid, 'Something else')
+
+ expect { database.flavor }.to raise_error ActiveRecord::StatementInvalid, /Something else/
+ end
+
+ it 'recognizes Amazon Aurora PostgreSQL' do
+ stub_statements(['SHOW rds.extensions', 'SELECT AURORA_VERSION()'])
+
+ expect(database.flavor).to eq('Amazon Aurora PostgreSQL')
+ end
+
+ it 'recognizes PostgreSQL on Amazon RDS' do
+ stub_statements('SHOW rds.extensions')
+
+ expect(database.flavor).to eq('PostgreSQL on Amazon RDS')
+ end
+
+ it 'recognizes CloudSQL for PostgreSQL' do
+ stub_statements('SHOW cloudsql.iam_authentication')
+
+ expect(database.flavor).to eq('Cloud SQL for PostgreSQL')
+ end
+
+ it 'recognizes Azure Database for PostgreSQL - Flexible Server' do
+ stub_statements(["SELECT datname FROM pg_database WHERE datname = 'azure_maintenance'", 'SHOW azure.extensions'])
+
+ expect(database.flavor).to eq('Azure Database for PostgreSQL - Flexible Server')
+ end
+
+ it 'recognizes Azure Database for PostgreSQL - Single Server' do
+ stub_statements("SELECT datname FROM pg_database WHERE datname = 'azure_maintenance'")
+
+ expect(database.flavor).to eq('Azure Database for PostgreSQL - Single Server')
+ end
+
+ it 'returns nil if can not recognize the flavor' do
+ expect(database.flavor).to be_nil
+ end
+ end
+
describe '#config' do
it 'returns a HashWithIndifferentAccess' do
expect(database.config)
diff --git a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
index 0afbe46b7f1..bb91617714a 100644
--- a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
@@ -6,30 +6,34 @@ RSpec.describe Gitlab::Database::Reindexing::Coordinator do
include Database::DatabaseHelpers
include ExclusiveLeaseHelpers
- describe '.perform' do
- subject { described_class.new(index, notifier).perform }
-
- let(:index) { create(:postgres_index) }
- let(:notifier) { instance_double(Gitlab::Database::Reindexing::GrafanaNotifier, notify_start: nil, notify_end: nil) }
- let(:reindexer) { instance_double(Gitlab::Database::Reindexing::ReindexConcurrently, perform: nil) }
- let(:action) { create(:reindex_action, index: index) }
+ let(:notifier) { instance_double(Gitlab::Database::Reindexing::GrafanaNotifier, notify_start: nil, notify_end: nil) }
+ let(:index) { create(:postgres_index) }
+ let(:connection) { index.connection }
- let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) }
- let(:lease_key) { "gitlab/database/reindexing/coordinator/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
- let(:lease_timeout) { 1.day }
- let(:uuid) { 'uuid' }
+ let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) }
+ let(:lease_key) { "gitlab/database/reindexing/coordinator/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_timeout) { 1.day }
+ let(:uuid) { 'uuid' }
- around do |example|
- model = Gitlab::Database.database_base_models[Gitlab::Database::PRIMARY_DATABASE_NAME]
+ around do |example|
+ model = Gitlab::Database.database_base_models[Gitlab::Database::PRIMARY_DATABASE_NAME]
- Gitlab::Database::SharedModel.using_connection(model.connection) do
- example.run
- end
+ Gitlab::Database::SharedModel.using_connection(model.connection) do
+ example.run
end
+ end
- before do
- swapout_view_for_table(:postgres_indexes)
+ before do
+ swapout_view_for_table(:postgres_indexes)
+ end
+ describe '#perform' do
+ subject { described_class.new(index, notifier).perform }
+
+ let(:reindexer) { instance_double(Gitlab::Database::Reindexing::ReindexConcurrently, perform: nil) }
+ let(:action) { create(:reindex_action, index: index) }
+
+ before do
allow(Gitlab::Database::Reindexing::ReindexConcurrently).to receive(:new).with(index).and_return(reindexer)
allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:create_for).with(index).and_return(action)
end
@@ -87,4 +91,40 @@ RSpec.describe Gitlab::Database::Reindexing::Coordinator do
end
end
end
+
+ describe '#drop' do
+ let(:connection) { index.connection }
+
+ subject(:drop) { described_class.new(index, notifier).drop }
+
+ context 'when exclusive lease is granted' do
+ it 'drops the index with lock retries' do
+ expect(lease).to receive(:try_obtain).ordered.and_return(uuid)
+
+ expect_query("SET lock_timeout TO '60000ms'")
+ expect_query("DROP INDEX CONCURRENTLY IF EXISTS \"public\".\"#{index.name}\"")
+ expect_query("RESET idle_in_transaction_session_timeout; RESET lock_timeout")
+
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid)
+
+ drop
+ end
+
+ def expect_query(sql)
+ expect(connection).to receive(:execute).ordered.with(sql).and_wrap_original do |method, sql|
+ method.call(sql.sub(/CONCURRENTLY/, ''))
+ end
+ end
+ end
+
+ context 'when exclusive lease is not granted' do
+ it 'does not drop the index' do
+ expect(lease).to receive(:try_obtain).ordered.and_return(false)
+ expect(Gitlab::Database::WithLockRetriesOutsideTransaction).not_to receive(:new)
+ expect(connection).not_to receive(:execute)
+
+ drop
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/failure_handler_spec.rb b/spec/lib/gitlab/email/failure_handler_spec.rb
new file mode 100644
index 00000000000..a912996e8f2
--- /dev/null
+++ b/spec/lib/gitlab/email/failure_handler_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::FailureHandler do
+ let(:raw_message) { fixture_file('emails/valid_reply.eml') }
+ let(:receiver) { Gitlab::Email::Receiver.new(raw_message) }
+
+ context 'email processing errors' do
+ where(:error, :message, :can_retry) do
+ [
+ [Gitlab::Email::UnknownIncomingEmail, "We couldn't figure out what the email is for", false],
+ [Gitlab::Email::SentNotificationNotFoundError, "We couldn't figure out what the email is in reply to", false],
+ [Gitlab::Email::ProjectNotFound, "We couldn't find the project", false],
+ [Gitlab::Email::EmptyEmailError, "It appears that the email is blank", true],
+ [Gitlab::Email::UserNotFoundError, "We couldn't figure out what user corresponds to the email", false],
+ [Gitlab::Email::UserBlockedError, "Your account has been blocked", false],
+ [Gitlab::Email::UserNotAuthorizedError, "You are not allowed to perform this action", false],
+ [Gitlab::Email::NoteableNotFoundError, "The thread you are replying to no longer exists", false],
+ [Gitlab::Email::InvalidAttachment, "Could not deal with that", false],
+ [Gitlab::Email::InvalidRecordError, "The note could not be created for the following reasons", true],
+ [Gitlab::Email::EmailTooLarge, "it is too large", false]
+ ]
+ end
+
+ with_them do
+ it "sends out a rejection email for #{params[:error]}" do
+ perform_enqueued_jobs do
+ described_class.handle(receiver, error.new(message))
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to match_array(["jake@adventuretime.ooo"])
+ expect(email.subject).to include("Rejected")
+ expect(email.body.parts.last.to_s).to include(message)
+ end
+
+ it 'strips out the body before passing to EmailRejectionMailer' do
+ mail = Mail.new(raw_message)
+ mail.body = nil
+
+ expect(EmailRejectionMailer).to receive(:rejection).with(match(message), mail.encoded, can_retry).and_call_original
+
+ described_class.handle(receiver, error.new(message))
+ end
+ end
+ end
+
+ context 'non-processing errors' do
+ where(:error) do
+ [
+ [Gitlab::Email::AutoGeneratedEmailError.new("")],
+ [ActiveRecord::StatementTimeout.new("StatementTimeout")],
+ [RateLimitedService::RateLimitedError.new(key: :issues_create, rate_limiter: nil)]
+ ]
+ end
+
+ with_them do
+ it "does not send a rejection email for #{params[:error]}" do
+ perform_enqueued_jobs do
+ described_class.handle(receiver, error)
+ end
+
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb
index af5f11c9362..3febc10831a 100644
--- a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb
@@ -178,5 +178,14 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do
expect(result_hash.dig(:extra, :sidekiq)).to be_nil
end
end
+
+ context 'when there is Sidekiq data but no job' do
+ let(:value) { { other: 'foo' } }
+ let(:wrapped_value) { { extra: { sidekiq: value } } }
+
+ it 'does nothing' do
+ expect(result_hash.dig(:extra, :sidekiq)).to eq(value)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/event_store/event_spec.rb b/spec/lib/gitlab/event_store/event_spec.rb
new file mode 100644
index 00000000000..97f6870a5ec
--- /dev/null
+++ b/spec/lib/gitlab/event_store/event_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::EventStore::Event do
+ let(:event_class) { stub_const('TestEvent', Class.new(described_class)) }
+ let(:event) { event_class.new(data: data) }
+ let(:data) { { project_id: 123, project_path: 'org/the-project' } }
+
+ context 'when schema is not defined' do
+ it 'raises an error on initialization' do
+ expect { event }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context 'when schema is defined' do
+ before do
+ event_class.class_eval do
+ def schema
+ {
+ 'required' => ['project_id'],
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'project_path' => { 'type' => 'string' }
+ }
+ }
+ end
+ end
+ end
+
+ describe 'schema validation' do
+ context 'when data matches the schema' do
+ it 'initializes the event correctly' do
+ expect(event.data).to eq(data)
+ end
+ end
+
+ context 'when required properties are present as well as unknown properties' do
+ let(:data) { { project_id: 123, unknown_key: 'unknown_value' } }
+
+ it 'initializes the event correctly' do
+ expect(event.data).to eq(data)
+ end
+ end
+
+ context 'when some properties are missing' do
+ let(:data) { { project_path: 'org/the-project' } }
+
+ it 'expects all properties to be present' do
+ expect { event }.to raise_error(Gitlab::EventStore::InvalidEvent, /does not match the defined schema/)
+ end
+ end
+
+ context 'when data is not a Hash' do
+ let(:data) { 123 }
+
+ it 'raises an error' do
+ expect { event }.to raise_error(Gitlab::EventStore::InvalidEvent, 'Event data must be a Hash')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/event_store/store_spec.rb b/spec/lib/gitlab/event_store/store_spec.rb
new file mode 100644
index 00000000000..711e1d5b4d5
--- /dev/null
+++ b/spec/lib/gitlab/event_store/store_spec.rb
@@ -0,0 +1,262 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::EventStore::Store do
+ let(:event_klass) { stub_const('TestEvent', Class.new(Gitlab::EventStore::Event)) }
+ let(:event) { event_klass.new(data: data) }
+ let(:another_event_klass) { stub_const('TestAnotherEvent', Class.new(Gitlab::EventStore::Event)) }
+
+ let(:worker) do
+ stub_const('EventSubscriber', Class.new).tap do |klass|
+ klass.class_eval do
+ include ApplicationWorker
+ include Gitlab::EventStore::Subscriber
+
+ def handle_event(event)
+ event.data
+ end
+ end
+ end
+ end
+
+ let(:another_worker) do
+ stub_const('AnotherEventSubscriber', Class.new).tap do |klass|
+ klass.class_eval do
+ include ApplicationWorker
+ include Gitlab::EventStore::Subscriber
+ end
+ end
+ end
+
+ let(:unrelated_worker) do
+ stub_const('UnrelatedEventSubscriber', Class.new).tap do |klass|
+ klass.class_eval do
+ include ApplicationWorker
+ include Gitlab::EventStore::Subscriber
+ end
+ end
+ end
+
+ before do
+ event_klass.class_eval do
+ def schema
+ {
+ 'required' => %w[name id],
+ 'type' => 'object',
+ 'properties' => {
+ 'name' => { 'type' => 'string' },
+ 'id' => { 'type' => 'integer' }
+ }
+ }
+ end
+ end
+ end
+
+ describe '#subscribe' do
+ it 'subscribes a worker to an event' do
+ store = described_class.new do |s|
+ s.subscribe worker, to: event_klass
+ end
+
+ subscriptions = store.subscriptions[event_klass]
+ expect(subscriptions.map(&:worker)).to contain_exactly(worker)
+ end
+
+ it 'subscribes multiple workers to an event' do
+ store = described_class.new do |s|
+ s.subscribe worker, to: event_klass
+ s.subscribe another_worker, to: event_klass
+ end
+
+ subscriptions = store.subscriptions[event_klass]
+ expect(subscriptions.map(&:worker)).to contain_exactly(worker, another_worker)
+ end
+
+ it 'subscribes a worker to multiple events is separate calls' do
+ store = described_class.new do |s|
+ s.subscribe worker, to: event_klass
+ s.subscribe worker, to: another_event_klass
+ end
+
+ subscriptions = store.subscriptions[event_klass]
+ expect(subscriptions.map(&:worker)).to contain_exactly(worker)
+
+ subscriptions = store.subscriptions[another_event_klass]
+ expect(subscriptions.map(&:worker)).to contain_exactly(worker)
+ end
+
+ it 'subscribes a worker to multiple events in a single call' do
+ store = described_class.new do |s|
+ s.subscribe worker, to: [event_klass, another_event_klass]
+ end
+
+ subscriptions = store.subscriptions[event_klass]
+ expect(subscriptions.map(&:worker)).to contain_exactly(worker)
+
+ subscriptions = store.subscriptions[another_event_klass]
+ expect(subscriptions.map(&:worker)).to contain_exactly(worker)
+ end
+
+ it 'subscribes a worker to an event with condition' do
+ store = described_class.new do |s|
+ s.subscribe worker, to: event_klass, if: ->(event) { event.data[:name] == 'Alice' }
+ end
+
+ subscriptions = store.subscriptions[event_klass]
+
+ expect(subscriptions.size).to eq(1)
+
+ subscription = subscriptions.first
+ expect(subscription).to be_an_instance_of(Gitlab::EventStore::Subscription)
+ expect(subscription.worker).to eq(worker)
+ expect(subscription.condition.call(double(data: { name: 'Bob' }))).to eq(false)
+ expect(subscription.condition.call(double(data: { name: 'Alice' }))).to eq(true)
+ end
+
+ it 'refuses the subscription if the target is not an Event object' do
+ expect do
+ described_class.new do |s|
+ s.subscribe worker, to: Integer
+ end
+ end.to raise_error(
+ Gitlab::EventStore::Error,
+ /Event being subscribed to is not a subclass of Gitlab::EventStore::Event/)
+ end
+
+ it 'refuses the subscription if the subscriber is not a worker' do
+ expect do
+ described_class.new do |s|
+ s.subscribe double, to: event_klass
+ end
+ end.to raise_error(
+ Gitlab::EventStore::Error,
+ /Subscriber is not an ApplicationWorker/)
+ end
+ end
+
+ describe '#publish' do
+ let(:data) { { name: 'Bob', id: 123 } }
+
+ context 'when event has a subscribed worker' do
+ let(:store) do
+ described_class.new do |store|
+ store.subscribe worker, to: event_klass
+ store.subscribe another_worker, to: another_event_klass
+ end
+ end
+
+ it 'dispatches the event to the subscribed worker' do
+ expect(worker).to receive(:perform_async).with('TestEvent', data)
+ expect(another_worker).not_to receive(:perform_async)
+
+ store.publish(event)
+ end
+
+ context 'when other workers subscribe to the same event' do
+ let(:store) do
+ described_class.new do |store|
+ store.subscribe worker, to: event_klass
+ store.subscribe another_worker, to: event_klass
+ store.subscribe unrelated_worker, to: another_event_klass
+ end
+ end
+
+ it 'dispatches the event to each subscribed worker' do
+ expect(worker).to receive(:perform_async).with('TestEvent', data)
+ expect(another_worker).to receive(:perform_async).with('TestEvent', data)
+ expect(unrelated_worker).not_to receive(:perform_async)
+
+ store.publish(event)
+ end
+ end
+
+ context 'when an error is raised' do
+ before do
+ allow(worker).to receive(:perform_async).and_raise(NoMethodError, 'the error message')
+ end
+
+ it 'is rescued and tracked' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_and_raise_for_dev_exception)
+ .with(kind_of(NoMethodError), event_class: event.class.name, event_data: event.data)
+ .and_call_original
+
+ expect { store.publish(event) }.to raise_error(NoMethodError, 'the error message')
+ end
+ end
+
+ it 'raises and tracks an error when event is published inside a database transaction' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_and_raise_for_dev_exception)
+ .at_least(:once)
+ .and_call_original
+
+ expect do
+ ApplicationRecord.transaction do
+ store.publish(event)
+ end
+ end.to raise_error(Sidekiq::Worker::EnqueueFromTransactionError)
+ end
+
+ it 'refuses publishing if the target is not an Event object' do
+ expect { store.publish(double(:event)) }
+ .to raise_error(
+ Gitlab::EventStore::Error,
+ /Event being published is not an instance of Gitlab::EventStore::Event/)
+ end
+ end
+
+ context 'when event has subscribed workers with condition' do
+ let(:store) do
+ described_class.new do |s|
+ s.subscribe worker, to: event_klass, if: -> (event) { event.data[:name] == 'Bob' }
+ s.subscribe another_worker, to: event_klass, if: -> (event) { event.data[:name] == 'Alice' }
+ end
+ end
+
+ let(:event) { event_klass.new(data: data) }
+
+ it 'dispatches the event to the workers satisfying the condition' do
+ expect(worker).to receive(:perform_async).with('TestEvent', data)
+ expect(another_worker).not_to receive(:perform_async)
+
+ store.publish(event)
+ end
+ end
+ end
+
+ describe 'subscriber' do
+ let(:data) { { name: 'Bob', id: 123 } }
+ let(:event_name) { event.class.name }
+ let(:worker_instance) { worker.new }
+
+ subject { worker_instance.perform(event_name, data) }
+
+ it 'handles the event' do
+ expect(worker_instance).to receive(:handle_event).with(instance_of(event.class))
+
+ expect_any_instance_of(event.class) do |event|
+ expect(event).to receive(:data).and_return(data)
+ end
+
+ subject
+ end
+
+ context 'when the event name does not exist' do
+ let(:event_name) { 'UnknownClass' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::EventStore::InvalidEvent)
+ end
+ end
+
+ context 'when the worker does not define handle_event method' do
+ let(:worker_instance) { another_worker.new }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/exceptions_app_spec.rb b/spec/lib/gitlab/exceptions_app_spec.rb
new file mode 100644
index 00000000000..6b726a044a8
--- /dev/null
+++ b/spec/lib/gitlab/exceptions_app_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ExceptionsApp, type: :request do
+ describe '.call' do
+ let(:exceptions_app) { described_class.new(Rails.public_path) }
+ let(:app) { ActionDispatch::ShowExceptions.new(error_raiser, exceptions_app) }
+
+ before do
+ @app = app
+ end
+
+ context 'for a 500 error' do
+ let(:error_raiser) { proc { raise 'an unhandled error' } }
+
+ context 'for an HTML request' do
+ it 'fills in the request ID' do
+ get '/', env: { 'action_dispatch.request_id' => 'foo' }
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response).to have_header('X-Gitlab-Custom-Error')
+ expect(response.body).to include('Request ID: <code>foo</code>')
+ end
+
+ it 'HTML-escapes the request ID' do
+ get '/', env: { 'action_dispatch.request_id' => '<b>foo</b>' }
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response).to have_header('X-Gitlab-Custom-Error')
+ expect(response.body).to include('Request ID: <code>&lt;b&gt;foo&lt;/b&gt;</code>')
+ end
+
+ it 'returns an empty 500 when the 500.html page cannot be found' do
+ allow(File).to receive(:exist?).and_return(false)
+
+ get '/', env: { 'action_dispatch.request_id' => 'foo' }
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response).not_to have_header('X-Gitlab-Custom-Error')
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'for a JSON request' do
+ it 'does not include the request ID' do
+ get '/', env: { 'action_dispatch.request_id' => 'foo' }, as: :json
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response).not_to have_header('X-Gitlab-Custom-Error')
+ expect(response.body).not_to include('foo')
+ end
+ end
+ end
+
+ context 'for a 404 error' do
+ let(:error_raiser) { proc { raise AbstractController::ActionNotFound } }
+
+ it 'returns a 404 response that does not include the request ID' do
+ get '/', env: { 'action_dispatch.request_id' => 'foo' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).not_to have_header('X-Gitlab-Custom-Error')
+ expect(response.body).not_to include('foo')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index f4875aa0ebc..7d4a3655be6 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
before do
- old_project.update(namespace: old_group)
+ old_project.update!(namespace: old_group)
end
context 'label referenced by id' do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index bf2e3c7f5f8..4bf7994f4dd 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when the DeployKey has access to the project' do
before do
- deploy_key.deploy_keys_projects.create(project: project, can_push: true)
+ deploy_key.deploy_keys_projects.create!(project: project, can_push: true)
end
it 'allows push and pull access' do
@@ -820,7 +820,7 @@ RSpec.describe Gitlab::GitAccess do
project.add_role(user, role)
end
- protected_branch.save
+ protected_branch.save!
aggregate_failures do
matrix.each do |action, allowed|
@@ -1090,7 +1090,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when deploy_key can push' do
context 'when project is authorized' do
before do
- key.deploy_keys_projects.create(project: project, can_push: true)
+ key.deploy_keys_projects.create!(project: project, can_push: true)
end
it { expect { push_access_check }.not_to raise_error }
@@ -1120,7 +1120,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when deploy_key cannot push' do
context 'when project is authorized' do
before do
- key.deploy_keys_projects.create(project: project, can_push: false)
+ key.deploy_keys_projects.create!(project: project, can_push: false)
end
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 20d5972bd88..9c399e78d80 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -233,30 +233,6 @@ RSpec.describe Gitlab::Gpg::Commit do
verification_status: 'multiple_signatures'
)
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(multiple_gpg_signatures: false)
- end
-
- it 'returns an valid signature' do
- verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
- allow(GPGME::Crypto).to receive(:new).and_return(crypto)
- allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature)
-
- signature = described_class.new(commit).signature
-
- expect(signature).to have_attributes(
- commit_sha: commit_sha,
- project: project,
- gpg_key: gpg_key,
- gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- gpg_key_user_name: GpgHelpers::User1.names.first,
- gpg_key_user_email: GpgHelpers::User1.emails.first,
- verification_status: 'verified'
- )
- end
- end
end
context 'commit signed with a subkey' do
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index d0aae2ac475..7d459f2d88a 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -29,14 +29,42 @@ RSpec.describe Gitlab::HTTP do
context 'when reading the response is too slow' do
before do
+ # Override Net::HTTP to add a delay between sending each response chunk
+ mocked_http = Class.new(Net::HTTP) do
+ def request(*)
+ super do |response|
+ response.instance_eval do
+ def read_body(*)
+ @body.each do |fragment|
+ sleep 0.002.seconds
+
+ yield fragment if block_given?
+ end
+ end
+ end
+
+ yield response if block_given?
+
+ response
+ end
+ end
+ end
+
+ @original_net_http = Net.send(:remove_const, :HTTP)
+ Net.send(:const_set, :HTTP, mocked_http)
+
stub_const("#{described_class}::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds)
WebMock.stub_request(:post, /.*/).to_return do |request|
- sleep 0.002.seconds
- { body: 'I\'m slow', status: 200 }
+ { body: %w(a b), status: 200 }
end
end
+ after do
+ Net.send(:remove_const, :HTTP)
+ Net.send(:const_set, :HTTP, @original_net_http)
+ end
+
let(:options) { {} }
subject(:request_slow_responder) { described_class.post('http://example.org', **options) }
@@ -51,7 +79,7 @@ RSpec.describe Gitlab::HTTP do
end
it 'still calls the block' do
- expect { |b| described_class.post('http://example.org', **options, &b) }.to yield_with_args
+ expect { |b| described_class.post('http://example.org', **options, &b) }.to yield_successive_args('a', 'b')
end
end
diff --git a/spec/lib/gitlab/import/set_async_jid_spec.rb b/spec/lib/gitlab/import/set_async_jid_spec.rb
index 016f7cac61a..6931a7a953d 100644
--- a/spec/lib/gitlab/import/set_async_jid_spec.rb
+++ b/spec/lib/gitlab/import/set_async_jid_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Import::SetAsyncJid do
it 'sets the JID in Redis' do
expect(Gitlab::SidekiqStatus)
.to receive(:set)
- .with("async-import/project-import-state/#{project.id}", Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION, value: 2)
+ .with("async-import/project-import-state/#{project.id}", Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
.and_call_original
described_class.set_jid(project.import_state)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7ed80cbcf66..f4a112d35aa 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -58,6 +58,7 @@ issues:
- test_reports
- requirement
- incident_management_issuable_escalation_status
+- incident_management_timeline_events
- pending_escalations
- customer_relations_contacts
- issue_customer_relations_contacts
@@ -135,6 +136,7 @@ project_members:
- source
- project
- member_task
+- member_namespace
merge_requests:
- status_check_responses
- subscriptions
@@ -280,6 +282,7 @@ ci_pipelines:
- dast_site_profiles_pipeline
- package_build_infos
- package_file_build_infos
+- build_trace_chunks
ci_refs:
- project
- ci_pipelines
@@ -601,6 +604,7 @@ project:
- bulk_import_exports
- ci_project_mirror
- sync_events
+- secure_files
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
index 334d930c47c..d897ce76da0 100644
--- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::ImportExport::AvatarSaver do
end
it 'saves a project avatar' do
- described_class.new(project: project_with_avatar, shared: shared).save
+ described_class.new(project: project_with_avatar, shared: shared).save # rubocop:disable Rails/SaveBang
expect(File).to exist(Dir["#{shared.export_path}/avatar/**/dk.png"].first)
end
diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
index bd8873fe20e..b8999f608b1 100644
--- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
let(:excluded_keys) { [] }
subject do
- described_class.create(relation_sym: relation_sym,
+ described_class.create(relation_sym: relation_sym, # rubocop:disable Rails/SaveBang
relation_hash: relation_hash,
relation_index: 1,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
index 6680f4e7a03..346f653acd4 100644
--- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
allow(instance).to receive(:storage_path).and_return(export_path)
end
- bundler.save
+ bundler.save # rubocop:disable Rails/SaveBang
end
after do
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index d5f31f235f5..adb613c3abc 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -258,7 +258,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
create(:resource_label_event, label: group_label, merge_request: merge_request)
create(:event, :created, target: milestone, project: project, author: user)
- create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' })
+ create(:integration, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' })
create(:project_custom_attribute, project: project)
create(:project_custom_attribute, project: project)
diff --git a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
index ce6607f6a26..2f1e2dd2db4 100644
--- a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
@@ -48,41 +48,16 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
subject { relation_tree_restorer.restore }
- shared_examples 'logging of relations creation' do
- context 'when log_import_export_relation_creation feature flag is enabled' do
- before do
- stub_feature_flags(log_import_export_relation_creation: group)
- end
-
- it 'logs top-level relation creation' do
- expect(shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .at_least(:once)
-
- subject
- end
- end
-
- context 'when log_import_export_relation_creation feature flag is disabled' do
- before do
- stub_feature_flags(log_import_export_relation_creation: false)
- end
-
- it 'does not log top-level relation creation' do
- expect(shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .never
-
- subject
- end
- end
- end
-
it 'restores group tree' do
expect(subject).to eq(true)
end
- include_examples 'logging of relations creation'
+ it 'logs top-level relation creation' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(hash_including(message: '[Project/Group Import] Created new object relation'))
+ .at_least(:once)
+
+ subject
+ end
end
diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
index 80ba50976af..ea8b10675af 100644
--- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
end
context 'original service exists' do
- let(:service_id) { create(:service, project: project).id }
+ let(:service_id) { create(:integration, project: project).id }
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
index 577f1e46db6..b7b652005e9 100644
--- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
@@ -54,38 +54,6 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
end
end
- shared_examples 'logging of relations creation' do
- context 'when log_import_export_relation_creation feature flag is enabled' do
- before do
- stub_feature_flags(log_import_export_relation_creation: group)
- end
-
- it 'logs top-level relation creation' do
- expect(shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .at_least(:once)
-
- subject
- end
- end
-
- context 'when log_import_export_relation_creation feature flag is disabled' do
- before do
- stub_feature_flags(log_import_export_relation_creation: false)
- end
-
- it 'does not log top-level relation creation' do
- expect(shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .never
-
- subject
- end
- end
- end
-
context 'with legacy reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:relation_reader) do
@@ -106,7 +74,14 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
end
- include_examples 'logging of relations creation'
+ it 'logs top-level relation creation' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(hash_including(message: '[Project/Group Import] Created new object relation'))
+ .at_least(:once)
+
+ subject
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 6ffe2187466..f019883a91e 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -692,6 +692,7 @@ Badge:
- type
ProjectCiCdSetting:
- group_runners_enabled
+- runner_token_expiration_interval
ProjectSetting:
- allow_merge_on_skipped_pipeline
- has_confluence
diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
index 8e9be209f89..bfb18c58806 100644
--- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::ImportExport::UploadsSaver do
end
it 'copies the uploads to the export path' do
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
uploads = Dir.glob(File.join(shared.export_path, 'uploads/**/*')).map { |file| File.basename(file) }
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::ImportExport::UploadsSaver do
end
it 'copies the uploads to the export path' do
- saver.save
+ saver.save # rubocop:disable Rails/SaveBang
uploads = Dir.glob(File.join(shared.export_path, 'uploads/**/*')).map { |file| File.basename(file) }
diff --git a/spec/lib/gitlab/integrations/sti_type_spec.rb b/spec/lib/gitlab/integrations/sti_type_spec.rb
index 70b93d6a4b5..1205b74dc9d 100644
--- a/spec/lib/gitlab/integrations/sti_type_spec.rb
+++ b/spec/lib/gitlab/integrations/sti_type_spec.rb
@@ -46,11 +46,11 @@ RSpec.describe Gitlab::Integrations::StiType do
SQL
end
- let_it_be(:service) { create(:service) }
+ let_it_be(:integration) { create(:integration) }
it 'forms SQL UPDATE statements correctly' do
sql_statements = types.map do |type|
- record = ActiveRecord::QueryRecorder.new { service.update_column(:type, type) }
+ record = ActiveRecord::QueryRecorder.new { integration.update_column(:type, type) }
record.log.first
end
@@ -65,8 +65,6 @@ RSpec.describe Gitlab::Integrations::StiType do
SQL
end
- let(:service) { create(:service) }
-
it 'forms SQL DELETE statements correctly' do
sql_statements = types.map do |type|
record = ActiveRecord::QueryRecorder.new { Integration.delete_by(type: type) }
@@ -81,7 +79,7 @@ RSpec.describe Gitlab::Integrations::StiType do
describe '#deserialize' do
specify 'it deserializes type correctly', :aggregate_failures do
types.each do |type|
- service = create(:service, type: type)
+ service = create(:integration, type: type)
expect(service.type).to eq('AsanaService')
end
@@ -90,7 +88,7 @@ RSpec.describe Gitlab::Integrations::StiType do
describe '#cast' do
it 'casts type as model correctly', :aggregate_failures do
- create(:service, type: 'AsanaService')
+ create(:integration, type: 'AsanaService')
types.each do |type|
expect(Integration.find_by(type: type)).to be_kind_of(Integrations::Asana)
@@ -100,7 +98,7 @@ RSpec.describe Gitlab::Integrations::StiType do
describe '#changed?' do
it 'detects changes correctly', :aggregate_failures do
- service = create(:service, type: 'AsanaService')
+ service = create(:integration, type: 'AsanaService')
types.each do |type|
service.type = type
diff --git a/spec/lib/gitlab/jwt_authenticatable_spec.rb b/spec/lib/gitlab/jwt_authenticatable_spec.rb
index 36bb46cb250..92d5feceb75 100644
--- a/spec/lib/gitlab/jwt_authenticatable_spec.rb
+++ b/spec/lib/gitlab/jwt_authenticatable_spec.rb
@@ -14,17 +14,12 @@ RSpec.describe Gitlab::JwtAuthenticatable do
end
before do
- begin
- File.delete(test_class.secret_path)
- rescue Errno::ENOENT
- end
+ FileUtils.rm_f(test_class.secret_path)
test_class.write_secret
end
- describe '.secret' do
- subject(:secret) { test_class.secret }
-
+ shared_examples 'reading secret from the secret path' do
it 'returns 32 bytes' do
expect(secret).to be_a(String)
expect(secret.length).to eq(32)
@@ -32,62 +27,170 @@ RSpec.describe Gitlab::JwtAuthenticatable do
end
it 'accepts a trailing newline' do
- File.open(test_class.secret_path, 'a') { |f| f.write "\n" }
+ File.open(secret_path, 'a') { |f| f.write "\n" }
expect(secret.length).to eq(32)
end
it 'raises an exception if the secret file cannot be read' do
- File.delete(test_class.secret_path)
+ File.delete(secret_path)
expect { secret }.to raise_exception(Errno::ENOENT)
end
it 'raises an exception if the secret file contains the wrong number of bytes' do
- File.truncate(test_class.secret_path, 0)
+ File.truncate(secret_path, 0)
expect { secret }.to raise_exception(RuntimeError)
end
end
+ describe '.secret' do
+ it_behaves_like 'reading secret from the secret path' do
+ subject(:secret) { test_class.secret }
+
+ let(:secret_path) { test_class.secret_path }
+ end
+ end
+
+ describe '.read_secret' do
+ it_behaves_like 'reading secret from the secret path' do
+ subject(:secret) { test_class.read_secret(secret_path) }
+
+ let(:secret_path) { test_class.secret_path }
+ end
+ end
+
describe '.write_secret' do
- it 'uses mode 0600' do
- expect(File.stat(test_class.secret_path).mode & 0777).to eq(0600)
+ context 'without an input' do
+ it 'uses mode 0600' do
+ expect(File.stat(test_class.secret_path).mode & 0777).to eq(0600)
+ end
+
+ it 'writes base64 data' do
+ bytes = Base64.strict_decode64(File.read(test_class.secret_path))
+
+ expect(bytes).not_to be_empty
+ end
end
- it 'writes base64 data' do
- bytes = Base64.strict_decode64(File.read(test_class.secret_path))
+ context 'with an input' do
+ let(:another_path) do
+ Rails.root.join('tmp', 'tests', '.jwt_another_shared_secret')
+ end
- expect(bytes).not_to be_empty
+ after do
+ File.delete(another_path)
+ rescue Errno::ENOENT
+ end
+
+ it 'uses mode 0600' do
+ test_class.write_secret(another_path)
+ expect(File.stat(another_path).mode & 0777).to eq(0600)
+ end
+
+ it 'writes base64 data' do
+ test_class.write_secret(another_path)
+ bytes = Base64.strict_decode64(File.read(another_path))
+
+ expect(bytes).not_to be_empty
+ end
end
end
- describe '.decode_jwt_for_issuer' do
- let(:payload) { { 'iss' => 'test_issuer' } }
+ describe '.decode_jwt' do |decode|
+ let(:payload) { {} }
+
+ context 'use included class secret' do
+ it 'accepts a correct header' do
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message) }.not_to raise_error
+ end
+
+ it 'raises an error when the JWT is not signed' do
+ encoded_message = JWT.encode(payload, nil, 'none')
+
+ expect { test_class.decode_jwt(encoded_message) }.to raise_error(JWT::DecodeError)
+ end
- it 'accepts a correct header' do
- encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+ it 'raises an error when the header is signed with the wrong secret' do
+ encoded_message = JWT.encode(payload, 'wrongsecret', 'HS256')
- expect { test_class.decode_jwt_for_issuer('test_issuer', encoded_message) }.not_to raise_error
+ expect { test_class.decode_jwt(encoded_message) }.to raise_error(JWT::DecodeError)
+ end
end
- it 'raises an error when the JWT is not signed' do
- encoded_message = JWT.encode(payload, nil, 'none')
+ context 'use an input secret' do
+ let(:another_secret) { 'another secret' }
+
+ it 'accepts a correct header' do
+ encoded_message = JWT.encode(payload, another_secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message, another_secret) }.not_to raise_error
+ end
- expect { test_class.decode_jwt_for_issuer('test_issuer', encoded_message) }.to raise_error(JWT::DecodeError)
+ it 'raises an error when the JWT is not signed' do
+ encoded_message = JWT.encode(payload, nil, 'none')
+
+ expect { test_class.decode_jwt(encoded_message, another_secret) }.to raise_error(JWT::DecodeError)
+ end
+
+ it 'raises an error when the header is signed with the wrong secret' do
+ encoded_message = JWT.encode(payload, 'wrongsecret', 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message, another_secret) }.to raise_error(JWT::DecodeError)
+ end
end
- it 'raises an error when the header is signed with the wrong secret' do
- encoded_message = JWT.encode(payload, 'wrongsecret', 'HS256')
+ context 'issuer option' do
+ let(:payload) { { 'iss' => 'test_issuer' } }
+
+ it 'returns decoded payload if issuer is correct' do
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+ payload = test_class.decode_jwt(encoded_message, issuer: 'test_issuer')
- expect { test_class.decode_jwt_for_issuer('test_issuer', encoded_message) }.to raise_error(JWT::DecodeError)
+ expect(payload[0]).to match a_hash_including('iss' => 'test_issuer')
+ end
+
+ it 'raises an error when the issuer is incorrect' do
+ payload['iss'] = 'somebody else'
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message, issuer: 'test_issuer') }.to raise_error(JWT::DecodeError)
+ end
end
- it 'raises an error when the issuer is incorrect' do
- payload['iss'] = 'somebody else'
- encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+ context 'iat_after option' do
+ it 'returns decoded payload if iat is valid' do
+ freeze_time do
+ encoded_message = JWT.encode(payload.merge(iat: (Time.current - 10.seconds).to_i), test_class.secret, 'HS256')
+ payload = test_class.decode_jwt(encoded_message, iat_after: Time.current - 20.seconds)
+
+ expect(payload[0]).to match a_hash_including('iat' => be_a(Integer))
+ end
+ end
+
+ it 'raises an error if iat is invalid' do
+ encoded_message = JWT.encode(payload.merge(iat: 'wrong'), test_class.secret, 'HS256')
- expect { test_class.decode_jwt_for_issuer('test_issuer', encoded_message) }.to raise_error(JWT::DecodeError)
+ expect { test_class.decode_jwt(encoded_message, iat_after: true) }.to raise_error(JWT::DecodeError)
+ end
+
+ it 'raises an error if iat is absent' do
+ encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
+
+ expect { test_class.decode_jwt(encoded_message, iat_after: true) }.to raise_error(JWT::DecodeError)
+ end
+
+ it 'raises an error if iat is too far in the past' do
+ freeze_time do
+ encoded_message = JWT.encode(payload.merge(iat: (Time.current - 30.seconds).to_i), test_class.secret, 'HS256')
+ expect do
+ test_class.decode_jwt(encoded_message, iat_after: Time.current - 20.seconds)
+ end.to raise_error(JWT::ExpiredSignature, 'Token has expired')
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
index f1284318687..1baf8749532 100644
--- a/spec/lib/gitlab/lets_encrypt/client_spec.rb
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe ::Gitlab::LetsEncrypt::Client do
context 'when private key is saved in settings' do
let!(:saved_private_key) do
key = OpenSSL::PKey::RSA.new(4096).to_pem
- Gitlab::CurrentSettings.current_application_settings.update(lets_encrypt_private_key: key)
+ Gitlab::CurrentSettings.current_application_settings.update!(lets_encrypt_private_key: key)
key
end
diff --git a/spec/lib/gitlab/lfs/client_spec.rb b/spec/lib/gitlab/lfs/client_spec.rb
index 0f9637e8ca4..db450c79dfa 100644
--- a/spec/lib/gitlab/lfs/client_spec.rb
+++ b/spec/lib/gitlab/lfs/client_spec.rb
@@ -114,6 +114,52 @@ RSpec.describe Gitlab::Lfs::Client do
end
end
+ context 'server returns 200 OK with a chunked transfer request' do
+ before do
+ upload_action['header']['Transfer-Encoding'] = 'gzip, chunked'
+ end
+
+ it "makes an HTTP PUT with expected parameters" do
+ stub_upload(object: object, headers: upload_action['header'], chunked_transfer: true).to_return(status: 200)
+
+ lfs_client.upload!(object, upload_action, authenticated: true)
+ end
+ end
+
+ context 'server returns 200 OK with a username and password in the URL' do
+ let(:base_url) { "https://someuser:testpass@example.com" }
+
+ it "makes an HTTP PUT with expected parameters" do
+ stub_upload(
+ object: object,
+ headers: basic_auth_headers.merge(upload_action['header']),
+ url: "https://example.com/some/file"
+ ).to_return(status: 200)
+
+ lfs_client.upload!(object, upload_action, authenticated: true)
+ end
+ end
+
+ context 'no credentials in client' do
+ subject(:lfs_client) { described_class.new(base_url, credentials: {}) }
+
+ context 'server returns 200 OK with credentials in URL' do
+ let(:creds) { 'someuser:testpass' }
+ let(:base_url) { "https://#{creds}@example.com" }
+ let(:auth_headers) { { 'Authorization' => "Basic #{Base64.strict_encode64(creds)}" } }
+
+ it "makes an HTTP PUT with expected parameters" do
+ stub_upload(
+ object: object,
+ headers: auth_headers.merge(upload_action['header']),
+ url: "https://example.com/some/file"
+ ).to_return(status: 200)
+
+ lfs_client.upload!(object, upload_action, authenticated: true)
+ end
+ end
+ end
+
context 'server returns 200 OK to an unauthenticated request' do
it "makes an HTTP PUT with expected parameters" do
stub = stub_upload(
@@ -159,7 +205,7 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_upload(object: object, headers: upload_action['header']).to_return(status: 400)
- expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed/)
+ expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed to upload object: HTTP status 400/)
end
end
@@ -167,20 +213,25 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_upload(object: object, headers: upload_action['header']).to_return(status: 500)
- expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed/)
+ expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed to upload object: HTTP status 500/)
end
end
- def stub_upload(object:, headers:)
+ def stub_upload(object:, headers:, url: upload_action['href'], chunked_transfer: false)
headers = {
'Content-Type' => 'application/octet-stream',
- 'Content-Length' => object.size.to_s,
'User-Agent' => git_lfs_user_agent
}.merge(headers)
- stub_request(:put, upload_action['href']).with(
+ if chunked_transfer
+ headers['Transfer-Encoding'] = 'gzip, chunked'
+ else
+ headers['Content-Length'] = object.size.to_s
+ end
+
+ stub_request(:put, url).with(
body: object.file.read,
- headers: headers.merge('Content-Length' => object.size.to_s)
+ headers: headers
)
end
end
@@ -196,11 +247,25 @@ RSpec.describe Gitlab::Lfs::Client do
end
end
+ context 'server returns 200 OK with a username and password in the URL' do
+ let(:base_url) { "https://someuser:testpass@example.com" }
+
+ it "makes an HTTP PUT with expected parameters" do
+ stub_verify(
+ object: object,
+ headers: basic_auth_headers.merge(verify_action['header']),
+ url: "https://example.com/some/file/verify"
+ ).to_return(status: 200)
+
+ lfs_client.verify!(object, verify_action, authenticated: true)
+ end
+ end
+
context 'server returns 200 OK to an unauthenticated request' do
it "makes an HTTP POST with expected parameters" do
stub = stub_verify(
object: object,
- headers: basic_auth_headers.merge(upload_action['header'])
+ headers: basic_auth_headers.merge(verify_action['header'])
).to_return(status: 200)
lfs_client.verify!(object, verify_action, authenticated: false)
@@ -226,7 +291,7 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_verify(object: object, headers: verify_action['header']).to_return(status: 400)
- expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed/)
+ expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed to verify object: HTTP status 400/)
end
end
@@ -234,18 +299,18 @@ RSpec.describe Gitlab::Lfs::Client do
it 'raises an error' do
stub_verify(object: object, headers: verify_action['header']).to_return(status: 500)
- expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed/)
+ expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed to verify object: HTTP status 500/)
end
end
- def stub_verify(object:, headers:)
+ def stub_verify(object:, headers:, url: verify_action['href'])
headers = {
'Accept' => git_lfs_content_type,
'Content-Type' => git_lfs_content_type,
'User-Agent' => git_lfs_user_agent
}.merge(headers)
- stub_request(:post, verify_action['href']).with(
+ stub_request(:post, url).with(
body: object.to_json(only: [:oid, :size]),
headers: headers
)
diff --git a/spec/lib/gitlab/logger_spec.rb b/spec/lib/gitlab/logger_spec.rb
new file mode 100644
index 00000000000..ed22af8355f
--- /dev/null
+++ b/spec/lib/gitlab/logger_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Logger do
+ describe '.build' do
+ before do
+ allow(described_class).to receive(:file_name_noext).and_return('log')
+ end
+
+ subject { described_class.build }
+
+ it 'builds logger using Gitlab::Logger.log_level' do
+ expect(described_class).to receive(:log_level).and_return(:warn)
+
+ expect(subject.level).to eq(described_class::WARN)
+ end
+
+ it 'raises ArgumentError if invalid log level' do
+ allow(described_class).to receive(:log_level).and_return(:invalid)
+
+ expect { subject.level }.to raise_error(ArgumentError, 'invalid log level: invalid')
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:env_value, :resulting_level) do
+ 0 | described_class::DEBUG
+ :debug | described_class::DEBUG
+ 'debug' | described_class::DEBUG
+ 'DEBUG' | described_class::DEBUG
+ 'DeBuG' | described_class::DEBUG
+ 1 | described_class::INFO
+ :info | described_class::INFO
+ 'info' | described_class::INFO
+ 'INFO' | described_class::INFO
+ 'InFo' | described_class::INFO
+ 2 | described_class::WARN
+ :warn | described_class::WARN
+ 'warn' | described_class::WARN
+ 'WARN' | described_class::WARN
+ 'WaRn' | described_class::WARN
+ 3 | described_class::ERROR
+ :error | described_class::ERROR
+ 'error' | described_class::ERROR
+ 'ERROR' | described_class::ERROR
+ 'ErRoR' | described_class::ERROR
+ 4 | described_class::FATAL
+ :fatal | described_class::FATAL
+ 'fatal' | described_class::FATAL
+ 'FATAL' | described_class::FATAL
+ 'FaTaL' | described_class::FATAL
+ 5 | described_class::UNKNOWN
+ :unknown | described_class::UNKNOWN
+ 'unknown' | described_class::UNKNOWN
+ 'UNKNOWN' | described_class::UNKNOWN
+ 'UnKnOwN' | described_class::UNKNOWN
+ end
+
+ with_them do
+ it 'builds logger if valid log level' do
+ stub_env('GITLAB_LOG_LEVEL', env_value)
+
+ expect(subject.level).to eq(resulting_level)
+ end
+ end
+ end
+
+ describe '.log_level' do
+ context 'if GITLAB_LOG_LEVEL is set' do
+ before do
+ stub_env('GITLAB_LOG_LEVEL', described_class::ERROR)
+ end
+
+ it 'returns value of GITLAB_LOG_LEVEL' do
+ expect(described_class.log_level).to eq(described_class::ERROR)
+ end
+
+ it 'ignores fallback' do
+ expect(described_class.log_level(fallback: described_class::FATAL)).to eq(described_class::ERROR)
+ end
+ end
+
+ context 'if GITLAB_LOG_LEVEL is not set' do
+ it 'returns default fallback DEBUG' do
+ expect(described_class.log_level).to eq(described_class::DEBUG)
+ end
+
+ it 'returns passed fallback' do
+ expect(described_class.log_level(fallback: described_class::FATAL)).to eq(described_class::FATAL)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/mail_room/authenticator_spec.rb b/spec/lib/gitlab/mail_room/authenticator_spec.rb
new file mode 100644
index 00000000000..44120902661
--- /dev/null
+++ b/spec/lib/gitlab/mail_room/authenticator_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::MailRoom::Authenticator do
+ let(:yml_config) do
+ {
+ enabled: true,
+ address: 'address@example.com'
+ }
+ end
+
+ let(:incoming_email_secret_path) { '/path/to/incoming_email_secret' }
+ let(:incoming_email_config) { yml_config.merge(secret_file: incoming_email_secret_path) }
+
+ let(:service_desk_email_secret_path) { '/path/to/service_desk_email_secret' }
+ let(:service_desk_email_config) { yml_config.merge(secret_file: service_desk_email_secret_path) }
+
+ let(:configs) do
+ {
+ incoming_email: incoming_email_config,
+ service_desk_email: service_desk_email_config
+ }
+ end
+
+ before do
+ allow(Gitlab::MailRoom).to receive(:enabled_configs).and_return(configs)
+
+ described_class.clear_memoization(:jwt_secret_incoming_email)
+ described_class.clear_memoization(:jwt_secret_service_desk_email)
+ end
+
+ after do
+ described_class.clear_memoization(:jwt_secret_incoming_email)
+ described_class.clear_memoization(:jwt_secret_service_desk_email)
+ end
+
+ around do |example|
+ freeze_time do
+ example.run
+ end
+ end
+
+ describe '#verify_api_request' do
+ let(:incoming_email_secret) { SecureRandom.hex(16) }
+ let(:service_desk_email_secret) { SecureRandom.hex(16) }
+ let(:payload) { { iss: described_class::INTERNAL_API_REQUEST_JWT_ISSUER, iat: (Time.current - 5.minutes + 1.second).to_i } }
+
+ before do
+ allow(described_class).to receive(:secret).with(:incoming_email).and_return(incoming_email_secret)
+ allow(described_class).to receive(:secret).with(:service_desk_email).and_return(service_desk_email_secret)
+ end
+
+ context 'verify a valid token' do
+ it 'returns the decoded payload' do
+ encoded_token = JWT.encode(payload, incoming_email_secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'incoming_email')[0]).to match a_hash_including(
+ "iss" => "gitlab-mailroom",
+ "iat" => be_a(Integer)
+ )
+
+ encoded_token = JWT.encode(payload, service_desk_email_secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'service_desk_email')[0]).to match a_hash_including(
+ "iss" => "gitlab-mailroom",
+ "iat" => be_a(Integer)
+ )
+ end
+ end
+
+ context 'verify an invalid token' do
+ it 'returns false' do
+ encoded_token = JWT.encode(payload, 'wrong secret', 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'incoming_email')).to eq(false)
+ end
+ end
+
+ context 'verify a valid token but wrong mailbox type' do
+ it 'returns false' do
+ encoded_token = JWT.encode(payload, incoming_email_secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'service_desk_email')).to eq(false)
+ end
+ end
+
+ context 'verify a valid token but wrong issuer' do
+ let(:payload) { { iss: 'invalid_issuer' } }
+
+ it 'returns false' do
+ encoded_token = JWT.encode(payload, incoming_email_secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'incoming_email')).to eq(false)
+ end
+ end
+
+ context 'verify a valid token but expired' do
+ let(:payload) { { iss: described_class::INTERNAL_API_REQUEST_JWT_ISSUER, iat: (Time.current - 5.minutes - 1.second).to_i } }
+
+ it 'returns false' do
+ encoded_token = JWT.encode(payload, incoming_email_secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'incoming_email')).to eq(false)
+ end
+ end
+
+ context 'verify a valid token but wrong header field' do
+ it 'returns false' do
+ encoded_token = JWT.encode(payload, incoming_email_secret, 'HS256')
+ headers = { 'a-wrong-header' => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'incoming_email')).to eq(false)
+ end
+ end
+
+ context 'verify headers for a disabled mailbox type' do
+ let(:configs) { { service_desk_email: service_desk_email_config } }
+
+ it 'returns false' do
+ encoded_token = JWT.encode(payload, incoming_email_secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers, 'incoming_email')).to eq(false)
+ end
+ end
+
+ context 'verify headers for a non-existing mailbox type' do
+ it 'returns false' do
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => 'something' }
+
+ expect(described_class.verify_api_request(headers, 'invalid_mailbox_type')).to eq(false)
+ end
+ end
+ end
+
+ describe '#secret' do
+ let(:incoming_email_secret) { SecureRandom.hex(16) }
+ let(:service_desk_email_secret) { SecureRandom.hex(16) }
+
+ context 'the secret is valid' do
+ before do
+ allow(described_class).to receive(:read_secret).with(incoming_email_secret_path).and_return(incoming_email_secret).once
+ allow(described_class).to receive(:read_secret).with(service_desk_email_secret_path).and_return(service_desk_email_secret).once
+ end
+
+ it 'returns the memorized secret from a file' do
+ expect(described_class.secret(:incoming_email)).to eql(incoming_email_secret)
+ # The second call does not trigger secret read again
+ expect(described_class.secret(:incoming_email)).to eql(incoming_email_secret)
+ expect(described_class).to have_received(:read_secret).with(incoming_email_secret_path).once
+
+ expect(described_class.secret(:service_desk_email)).to eql(service_desk_email_secret)
+ # The second call does not trigger secret read again
+ expect(described_class.secret(:service_desk_email)).to eql(service_desk_email_secret)
+ expect(described_class).to have_received(:read_secret).with(service_desk_email_secret_path).once
+ end
+ end
+
+ context 'the secret file is not configured' do
+ let(:incoming_email_config) { yml_config }
+
+ it 'raises a SecretConfigurationError exception' do
+ expect do
+ described_class.secret(:incoming_email)
+ end.to raise_error(described_class::SecretConfigurationError, "incoming_email's secret_file configuration is missing")
+ end
+ end
+
+ context 'the secret file not found' do
+ before do
+ allow(described_class).to receive(:read_secret).with(incoming_email_secret_path).and_raise(Errno::ENOENT)
+ end
+
+ it 'raises a SecretConfigurationError exception' do
+ expect do
+ described_class.secret(:incoming_email)
+ end.to raise_error(described_class::SecretConfigurationError, "Fail to read incoming_email's secret: No such file or directory")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb
index 0bd1a27c65e..a4fcf71a012 100644
--- a/spec/lib/gitlab/mail_room/mail_room_spec.rb
+++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::MailRoom do
end
before do
+ allow(described_class).to receive(:load_yaml).and_return(configs)
described_class.instance_variable_set(:@enabled_configs, nil)
end
@@ -38,10 +39,6 @@ RSpec.describe Gitlab::MailRoom do
end
describe '#enabled_configs' do
- before do
- allow(described_class).to receive(:load_yaml).and_return(configs)
- end
-
context 'when both email and address is set' do
it 'returns email configs' do
expect(described_class.enabled_configs.size).to eq(2)
@@ -79,7 +76,7 @@ RSpec.describe Gitlab::MailRoom do
let(:custom_config) { { enabled: true, address: 'address@example.com' } }
it 'overwrites missing values with the default' do
- expect(described_class.enabled_configs.first[:port]).to eq(Gitlab::MailRoom::DEFAULT_CONFIG[:port])
+ expect(described_class.enabled_configs.each_value.first[:port]).to eq(Gitlab::MailRoom::DEFAULT_CONFIG[:port])
end
end
@@ -88,7 +85,7 @@ RSpec.describe Gitlab::MailRoom do
it 'returns only encoming_email' do
expect(described_class.enabled_configs.size).to eq(1)
- expect(described_class.enabled_configs.first[:worker]).to eq('EmailReceiverWorker')
+ expect(described_class.enabled_configs.each_value.first[:worker]).to eq('EmailReceiverWorker')
end
end
@@ -100,11 +97,12 @@ RSpec.describe Gitlab::MailRoom do
end
it 'sets redis config' do
- config = described_class.enabled_configs.first
-
- expect(config[:redis_url]).to eq('localhost')
- expect(config[:redis_db]).to eq(99)
- expect(config[:sentinels]).to eq('yes, them')
+ config = described_class.enabled_configs.each_value.first
+ expect(config).to include(
+ redis_url: 'localhost',
+ redis_db: 99,
+ sentinels: 'yes, them'
+ )
end
end
@@ -113,7 +111,7 @@ RSpec.describe Gitlab::MailRoom do
let(:custom_config) { { log_path: 'tiny_log.log' } }
it 'expands the log path to an absolute value' do
- new_path = Pathname.new(described_class.enabled_configs.first[:log_path])
+ new_path = Pathname.new(described_class.enabled_configs.each_value.first[:log_path])
expect(new_path.absolute?).to be_truthy
end
end
@@ -122,9 +120,48 @@ RSpec.describe Gitlab::MailRoom do
let(:custom_config) { { log_path: '/dev/null' } }
it 'leaves the path as-is' do
- expect(described_class.enabled_configs.first[:log_path]).to eq '/dev/null'
+ expect(described_class.enabled_configs.each_value.first[:log_path]).to eq '/dev/null'
end
end
end
end
+
+ describe '#enabled_mailbox_types' do
+ context 'when all mailbox types are enabled' do
+ it 'returns the mailbox types' do
+ expect(described_class.enabled_mailbox_types).to match(%w[incoming_email service_desk_email])
+ end
+ end
+
+ context 'when an mailbox_types is disabled' do
+ let(:incoming_email_config) { yml_config.merge(enabled: false) }
+
+ it 'returns the mailbox types' do
+ expect(described_class.enabled_mailbox_types).to match(%w[service_desk_email])
+ end
+ end
+
+ context 'when email is disabled' do
+ let(:custom_config) { { enabled: false } }
+
+ it 'returns an empty array' do
+ expect(described_class.enabled_mailbox_types).to match_array([])
+ end
+ end
+ end
+
+ describe '#worker_for' do
+ context 'matched mailbox types' do
+ it 'returns the constantized worker class' do
+ expect(described_class.worker_for('incoming_email')).to eql(EmailReceiverWorker)
+ expect(described_class.worker_for('service_desk_email')).to eql(ServiceDeskEmailReceiverWorker)
+ end
+ end
+
+ context 'non-existing mailbox_type' do
+ it 'returns nil' do
+ expect(described_class.worker_for('another_mailbox_type')).to be(nil)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb b/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
index 65c76aac10c..2407b497249 100644
--- a/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
+++ b/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
@@ -15,7 +15,8 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
)
end
- let(:user) { project.creator }
+ let(:current_user) { create(:user, name: 'John Doe', email: 'john.doe@example.com') }
+ let(:author) { project.creator }
let(:source_branch) { 'feature' }
let(:merge_request_description) { "Merge Request Description\nNext line" }
let(:merge_request_title) { 'Bugfix' }
@@ -27,13 +28,13 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
target_project: project,
target_branch: 'master',
source_branch: source_branch,
- author: user,
+ author: author,
description: merge_request_description,
title: merge_request_title
)
end
- subject { described_class.new(merge_request: merge_request) }
+ subject { described_class.new(merge_request: merge_request, current_user: current_user) }
shared_examples_for 'commit message with template' do |message_template_name|
it 'returns nil when template is not set in target project' do
@@ -56,6 +57,19 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
end
+ context 'when project has commit template with only the title' do
+ let(:merge_request) do
+ double(:merge_request, title: 'Fixes', target_project: project, to_reference: '!123', metrics: nil, merge_user: nil)
+ end
+
+ let(message_template_name) { '%{title}' }
+
+ it 'evaluates only necessary variables' do
+ expect(result_message).to eq 'Fixes'
+ expect(merge_request).not_to have_received(:to_reference)
+ end
+ end
+
context 'when project has commit template with closed issues' do
let(message_template_name) { <<~MSG.rstrip }
Merge branch '%{source_branch}' into '%{target_branch}'
@@ -274,17 +288,319 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
end
end
+
+ context 'when project has merge commit template with approvers' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(message_template_name) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{approved_by}
+ MSG
+
+ context 'and mr has no approval' do
+ before do
+ merge_request.approved_by_users = []
+ end
+
+ it 'removes variable and blank line' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+ MSG
+ end
+
+ context 'when there is blank line after approved_by' do
+ let(message_template_name) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{approved_by}
+
+ Type: merge
+ MSG
+
+ it 'removes blank line before it' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Type: merge
+ MSG
+ end
+ end
+
+ context 'when there is no blank line after approved_by' do
+ let(message_template_name) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{approved_by}
+ Type: merge
+ MSG
+
+ it 'does not remove blank line before it' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Type: merge
+ MSG
+ end
+ end
+ end
+
+ context 'and mr has one approval' do
+ before do
+ merge_request.approved_by_users = [user1]
+ end
+
+ it 'returns user name and email' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Approved-by: #{user1.name} <#{user1.email}>
+ MSG
+ end
+ end
+
+ context 'and mr has multiple approvals' do
+ before do
+ merge_request.approved_by_users = [user1, user2]
+ end
+
+ it 'returns users names and emails' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Approved-by: #{user1.name} <#{user1.email}>
+ Approved-by: #{user2.name} <#{user2.email}>
+ MSG
+ end
+ end
+ end
+
+ context 'when project has merge commit template with url' do
+ let(message_template_name) do
+ "Merge Request URL is '%{url}'"
+ end
+
+ context "and merge request has url" do
+ it "returns mr url" do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge Request URL is '#{Gitlab::UrlBuilder.build(merge_request)}'
+ MSG
+ end
+ end
+ end
+
+ context 'when project has merge commit template with merged_by' do
+ let(message_template_name) do
+ "Merge Request merged by '%{merged_by}'"
+ end
+
+ context "and current_user is passed" do
+ it "returns user name and email" do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge Request merged by '#{current_user.name} <#{current_user.email}>'
+ MSG
+ end
+ end
+ end
+
+ context 'user' do
+ subject { described_class.new(merge_request: merge_request, current_user: nil) }
+
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(message_template_name) do
+ "Merge Request merged by '%{merged_by}'"
+ end
+
+ context 'comes from metrics' do
+ before do
+ merge_request.metrics.merged_by = user1
+ end
+
+ it "returns user name and email" do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge Request merged by '#{user1.name} <#{user1.email}>'
+ MSG
+ end
+ end
+
+ context 'comes from merge_user' do
+ before do
+ merge_request.merge_user = user2
+ end
+
+ it "returns user name and email" do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge Request merged by '#{user2.name} <#{user2.email}>'
+ MSG
+ end
+ end
+ end
+
+ context 'when project has commit template with the same variable used twice' do
+ let(message_template_name) { '%{title} %{title}' }
+
+ it 'uses custom template' do
+ expect(result_message).to eq 'Bugfix Bugfix'
+ end
+ end
+
+ context 'when project has commit template without any variable' do
+ let(message_template_name) { 'static text' }
+
+ it 'uses custom template' do
+ expect(result_message).to eq 'static text'
+ end
+ end
+
+ context 'when project has template with all variables' do
+ let(message_template_name) { <<~MSG.rstrip }
+ source_branch:%{source_branch}
+ target_branch:%{target_branch}
+ title:%{title}
+ issues:%{issues}
+ description:%{description}
+ first_commit:%{first_commit}
+ first_multiline_commit:%{first_multiline_commit}
+ url:%{url}
+ approved_by:%{approved_by}
+ merged_by:%{merged_by}
+ co_authored_by:%{co_authored_by}
+ MSG
+
+ it 'uses custom template' do
+ expect(result_message).to eq <<~MSG.rstrip
+ source_branch:feature
+ target_branch:master
+ title:Bugfix
+ issues:
+ description:Merge Request Description
+ Next line
+ first_commit:Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ first_multiline_commit:Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ url:#{Gitlab::UrlBuilder.build(merge_request)}
+ approved_by:
+ merged_by:#{current_user.name} <#{current_user.commit_email_or_default}>
+ co_authored_by:Co-authored-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ MSG
+ end
+ end
+
+ context 'when project has merge commit template with co_authored_by' do
+ let(:source_branch) { 'signed-commits' }
+ let(message_template_name) { <<~MSG.rstrip }
+ %{title}
+
+ %{co_authored_by}
+ MSG
+
+ it 'uses custom template' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Bugfix
+
+ Co-authored-by: Nannie Bernhard <nannie.bernhard@example.com>
+ Co-authored-by: Winnie Hellmann <winnie@gitlab.com>
+ MSG
+ end
+
+ context 'when author and merging user is one of the commit authors' do
+ let(:author) { create(:user, email: 'nannie.bernhard@example.com') }
+
+ before do
+ merge_request.merge_user = author
+ end
+
+ it 'skips his mail in coauthors' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Bugfix
+
+ Co-authored-by: Winnie Hellmann <winnie@gitlab.com>
+ MSG
+ end
+ end
+
+ context 'when author and merging user is the only author of commits' do
+ let(:author) { create(:user, email: 'dmitriy.zaporozhets@gmail.com') }
+ let(:source_branch) { 'feature' }
+
+ before do
+ merge_request.merge_user = author
+ end
+
+ it 'skips coauthors and empty lines before it' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Bugfix
+ MSG
+ end
+ end
+ end
end
describe '#merge_message' do
let(:result_message) { subject.merge_message }
it_behaves_like 'commit message with template', :merge_commit_template
+
+ context 'when project has merge commit template with co_authored_by' do
+ let(:source_branch) { 'signed-commits' }
+ let(:merge_commit_template) { <<~MSG.rstrip }
+ %{title}
+
+ %{co_authored_by}
+ MSG
+
+ context 'when author and merging user are one of the commit authors' do
+ let(:author) { create(:user, email: 'nannie.bernhard@example.com') }
+ let(:merge_user) { create(:user, email: 'winnie@gitlab.com') }
+
+ before do
+ merge_request.merge_user = merge_user
+ end
+
+ it 'skips merging user, but does not skip merge request author' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Bugfix
+
+ Co-authored-by: Nannie Bernhard <nannie.bernhard@example.com>
+ MSG
+ end
+ end
+ end
end
describe '#squash_message' do
let(:result_message) { subject.squash_message }
it_behaves_like 'commit message with template', :squash_commit_template
+
+ context 'when project has merge commit template with co_authored_by' do
+ let(:source_branch) { 'signed-commits' }
+ let(:squash_commit_template) { <<~MSG.rstrip }
+ %{title}
+
+ %{co_authored_by}
+ MSG
+
+ context 'when author and merging user are one of the commit authors' do
+ let(:author) { create(:user, email: 'nannie.bernhard@example.com') }
+ let(:merge_user) { create(:user, email: 'winnie@gitlab.com') }
+
+ before do
+ merge_request.merge_user = merge_user
+ end
+
+ it 'skips merge request author, but does not skip merging user' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Bugfix
+
+ Co-authored-by: Winnie Hellmann <winnie@gitlab.com>
+ MSG
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index 9cd1ef4094e..c7afc02f0af 100644
--- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -4,13 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
let(:settings) { double('settings') }
- let(:exporter) { described_class.new(settings) }
- let(:log_filename) { File.join(Rails.root, 'log', 'sidekiq_exporter.log') }
-
- before do
- allow_any_instance_of(described_class).to receive(:log_filename).and_return(log_filename)
- allow_any_instance_of(described_class).to receive(:settings).and_return(settings)
- end
+ let(:log_enabled) { false }
+ let(:exporter) { described_class.new(settings, log_enabled: log_enabled, log_file: 'test_exporter.log') }
describe 'when exporter is enabled' do
before do
@@ -61,6 +56,38 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
exporter.start.join
end
+
+ context 'logging enabled' do
+ let(:log_enabled) { true }
+ let(:logger) { instance_double(WEBrick::Log) }
+
+ before do
+ allow(logger).to receive(:time_format=)
+ allow(logger).to receive(:info)
+ end
+
+ it 'configures a WEBrick logger with the given file' do
+ expect(WEBrick::Log).to receive(:new).with(end_with('test_exporter.log')).and_return(logger)
+
+ exporter
+ end
+
+ it 'logs any errors during startup' do
+ expect(::WEBrick::Log).to receive(:new).and_return(logger)
+ expect(::WEBrick::HTTPServer).to receive(:new).and_raise 'fail'
+ expect(logger).to receive(:error)
+
+ exporter.start
+ end
+ end
+
+ context 'logging disabled' do
+ it 'configures a WEBrick logger with the null device' do
+ expect(WEBrick::Log).to receive(:new).with(File::NULL).and_call_original
+
+ exporter
+ end
+ end
end
describe 'when thread is not alive' do
@@ -111,6 +138,18 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
describe 'request handling' do
using RSpec::Parameterized::TableSyntax
+ let(:fake_collector) do
+ Class.new do
+ def initialize(app, ...)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ end
+ end
+ end
+
where(:method_class, :path, :http_status) do
Net::HTTP::Get | '/metrics' | 200
Net::HTTP::Get | '/liveness' | 200
@@ -123,6 +162,8 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
allow(settings).to receive(:port).and_return(0)
allow(settings).to receive(:address).and_return('127.0.0.1')
+ stub_const('Gitlab::Metrics::Exporter::MetricsMiddleware', fake_collector)
+
# We want to wrap original method
# and run handling of requests
# in separate thread
@@ -134,8 +175,6 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
# is raised as we close listeners
end
end
-
- exporter.start.join
end
after do
@@ -146,12 +185,25 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter do
let(:config) { exporter.server.config }
let(:request) { method_class.new(path) }
- it 'responds with proper http_status' do
+ subject(:response) do
http = Net::HTTP.new(config[:BindAddress], config[:Port])
- response = http.request(request)
+ http.request(request)
+ end
+
+ it 'responds with proper http_status' do
+ exporter.start.join
expect(response.code).to eq(http_status.to_s)
end
+
+ it 'collects request metrics' do
+ expect_next_instance_of(fake_collector) do |instance|
+ expect(instance).to receive(:call).and_call_original
+ end
+
+ exporter.start.join
+ response
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb b/spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb
new file mode 100644
index 00000000000..0c70a5de701
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/gc_request_middleware_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Metrics::Exporter::GcRequestMiddleware do
+ let(:app) { double(:app) }
+ let(:env) { {} }
+
+ subject(:middleware) { described_class.new(app) }
+
+ describe '#call' do
+ it 'runs a major GC after the next middleware is called' do
+ expect(app).to receive(:call).with(env).ordered.and_return([200, {}, []])
+ expect(GC).to receive(:start).ordered
+
+ response = middleware.call(env)
+
+ expect(response).to eq([200, {}, []])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/health_checks_middleware_spec.rb b/spec/lib/gitlab/metrics/exporter/health_checks_middleware_spec.rb
new file mode 100644
index 00000000000..9ee46a45e7a
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/health_checks_middleware_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Metrics::Exporter::HealthChecksMiddleware do
+ let(:app) { double(:app) }
+ let(:env) { { 'PATH_INFO' => path } }
+
+ let(:readiness_probe) { double(:readiness_probe) }
+ let(:liveness_probe) { double(:liveness_probe) }
+ let(:probe_result) { Gitlab::HealthChecks::Probes::Status.new(200, { status: 'ok' }) }
+
+ subject(:middleware) { described_class.new(app, readiness_probe, liveness_probe) }
+
+ describe '#call' do
+ context 'handling /readiness requests' do
+ let(:path) { '/readiness' }
+
+ it 'handles the request' do
+ expect(readiness_probe).to receive(:execute).and_return(probe_result)
+
+ response = middleware.call(env)
+
+ expect(response).to eq([200, { 'Content-Type' => 'application/json; charset=utf-8' }, ['{"status":"ok"}']])
+ end
+ end
+
+ context 'handling /liveness requests' do
+ let(:path) { '/liveness' }
+
+ it 'handles the request' do
+ expect(liveness_probe).to receive(:execute).and_return(probe_result)
+
+ response = middleware.call(env)
+
+ expect(response).to eq([200, { 'Content-Type' => 'application/json; charset=utf-8' }, ['{"status":"ok"}']])
+ end
+ end
+
+ context 'handling other requests' do
+ let(:path) { '/other_path' }
+
+ it 'forwards them to the next middleware' do
+ expect(app).to receive(:call).with(env).and_return([201, {}, []])
+
+ response = middleware.call(env)
+
+ expect(response).to eq([201, {}, []])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/metrics_middleware_spec.rb b/spec/lib/gitlab/metrics/exporter/metrics_middleware_spec.rb
new file mode 100644
index 00000000000..ac5721f5974
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/metrics_middleware_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Metrics::Exporter::MetricsMiddleware do
+ let(:app) { double(:app) }
+ let(:pid) { 'fake_exporter' }
+ let(:env) { { 'PATH_INFO' => '/path', 'REQUEST_METHOD' => 'GET' } }
+
+ subject(:middleware) { described_class.new(app, pid) }
+
+ def metric(name, method, path, status)
+ metric = ::Prometheus::Client.registry.get(name)
+ return unless metric
+
+ values = metric.values.transform_keys { |k| k.slice(:method, :path, :pid, :code) }
+ values[{ method: method, path: path, pid: pid, code: status.to_s }]&.get
+ end
+
+ before do
+ expect(app).to receive(:call).with(env).and_return([200, {}, []])
+ end
+
+ describe '#call', :prometheus do
+ it 'records a total requests metric' do
+ response = middleware.call(env)
+
+ expect(response).to eq([200, {}, []])
+ expect(metric(:exporter_http_requests_total, 'get', '/path', 200)).to eq(1.0)
+ end
+
+ it 'records a request duration histogram' do
+ response = middleware.call(env)
+
+ expect(response).to eq([200, {}, []])
+ expect(metric(:exporter_http_request_duration_seconds, 'get', '/path', 200)).to be_a(Hash)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
deleted file mode 100644
index 75bc3ba9626..00000000000
--- a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Metrics::Exporter::SidekiqExporter do
- let(:exporter) { described_class.new(Settings.monitoring.sidekiq_exporter) }
-
- after do
- exporter.stop
- end
-
- context 'with valid config' do
- before do
- stub_config(
- monitoring: {
- sidekiq_exporter: {
- enabled: true,
- log_enabled: false,
- port: 0,
- address: '127.0.0.1'
- }
- }
- )
- end
-
- it 'does start thread' do
- expect(exporter.start).not_to be_nil
- end
-
- it 'does not enable logging by default' do
- expect(exporter.log_filename).to eq(File::NULL)
- end
- end
-
- context 'with logging enabled' do
- before do
- stub_config(
- monitoring: {
- sidekiq_exporter: {
- enabled: true,
- log_enabled: true,
- port: 0,
- address: '127.0.0.1'
- }
- }
- )
- end
-
- it 'returns a valid log filename' do
- expect(exporter.log_filename).to end_with('sidekiq_exporter.log')
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
index 9deaecbf41b..0531bccf4b4 100644
--- a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
@@ -24,14 +24,14 @@ RSpec.describe Gitlab::Metrics::Exporter::WebExporter do
exporter.stop
end
- context 'when running server' do
+ context 'when running server', :prometheus do
it 'readiness probe returns succesful status' do
expect(readiness_probe.http_status).to eq(200)
expect(readiness_probe.json).to include(status: 'ok')
expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'ok' }])
end
- it 'initializes request metrics', :prometheus do
+ it 'initializes request metrics' do
expect(Gitlab::Metrics::RailsSlis).to receive(:initialize_request_slis_if_needed!).and_call_original
http = Net::HTTP.new(exporter.server.config[:BindAddress], exporter.server.config[:Port])
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::Metrics::Exporter::WebExporter do
end
describe '#mark_as_not_running!' do
- it 'readiness probe returns a failure status' do
+ it 'readiness probe returns a failure status', :prometheus do
exporter.mark_as_not_running!
expect(readiness_probe.http_status).to eq(503)
diff --git a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb
index d834b796179..e1e4877cd50 100644
--- a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::Samplers::ActionCableSampler do
let(:action_cable) { instance_double(ActionCable::Server::Base) }
- subject { described_class.new(action_cable: action_cable) }
+ subject { described_class.new(action_cable: action_cable, logger: double) }
it_behaves_like 'metrics sampler', 'ACTION_CABLE_SAMPLER'
diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
index e8f8947c9e8..c88d8c17eac 100644
--- a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
end
context 'when replica hosts are configured' do
- let(:main_load_balancer) { ActiveRecord::Base.load_balancer } # rubocop:disable Database/MultipleDatabases
+ let(:main_load_balancer) { ApplicationRecord.load_balancer }
let(:main_replica_host) { main_load_balancer.host }
let(:ci_load_balancer) { double(:load_balancer, host_list: ci_host_list, configuration: configuration) }
@@ -117,7 +117,7 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
end
context 'when the base model has replica connections' do
- let(:main_load_balancer) { ActiveRecord::Base.load_balancer } # rubocop:disable Database/MultipleDatabases
+ let(:main_load_balancer) { ApplicationRecord.load_balancer }
let(:main_replica_host) { main_load_balancer.host }
let(:ci_load_balancer) { double(:load_balancer, host_list: ci_host_list, configuration: configuration) }
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 6f1e0480197..a4877208bcf 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do
end
describe '#sample_gc' do
- let!(:sampler) { described_class.new(5) }
+ let!(:sampler) { described_class.new }
let(:gc_reports) { [{ GC_TIME: 0.1 }, { GC_TIME: 0.2 }, { GC_TIME: 0.3 }] }
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 1ef548ab29b..bc1d53b2ccb 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::Middleware::Go do
context 'without access to the project', :sidekiq_inline do
before do
- project.team.find_member(current_user).destroy
+ project.team.find_member(current_user).destroy!
end
it_behaves_like 'unauthorized'
diff --git a/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb b/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb
new file mode 100644
index 00000000000..c8dbc990f8c
--- /dev/null
+++ b/spec/lib/gitlab/middleware/webhook_recursion_detection_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'action_dispatch'
+require 'rack'
+require 'request_store'
+
+RSpec.describe Gitlab::Middleware::WebhookRecursionDetection do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
+
+ around do |example|
+ Gitlab::WithRequestStore.with_request_store { example.run }
+ end
+
+ describe '#call' do
+ subject(:call) { described_class.new(app).call(env) }
+
+ context 'when the recursion detection header is present' do
+ let(:new_uuid) { SecureRandom.uuid }
+ let(:headers) { { 'HTTP_X_GITLAB_EVENT_UUID' => new_uuid } }
+
+ it 'sets the request UUID from the header' do
+ expect(app).to receive(:call)
+ expect { call }.to change { Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid }.to(new_uuid)
+ end
+ end
+
+ context 'when recursion headers are not present' do
+ let(:headers) { {} }
+
+ it 'works without errors' do
+ expect(app).to receive(:call)
+
+ call
+
+ expect(Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data_spec.rb
new file mode 100644
index 00000000000..b4869f49081
--- /dev/null
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumnData do
+ let(:arel_table) { Issue.arel_table }
+
+ let(:column) do
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ column_expression: arel_table[:id],
+ order_expression: arel_table[:id].desc
+ )
+ end
+
+ subject(:column_data) { described_class.new(column, 'column_alias', arel_table) }
+
+ describe '#arel_column' do
+ it 'delegates to column_expression' do
+ expect(column_data.arel_column).to eq(column.column_expression)
+ end
+ end
+
+ describe '#column_for_projection' do
+ it 'returns the expression with AS using the original column name' do
+ expect(column_data.column_for_projection.to_sql).to eq('"issues"."id" AS id')
+ end
+ end
+
+ describe '#projection' do
+ it 'returns the expression with AS using the specified column lias' do
+ expect(column_data.projection.to_sql).to eq('"issues"."id" AS column_alias')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
index 00beacd4b35..58db22e5a9c 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
@@ -33,14 +33,14 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
]
end
- shared_examples 'correct ordering examples' do
- let(:iterator) do
- Gitlab::Pagination::Keyset::Iterator.new(
- scope: scope.limit(batch_size),
- in_operator_optimization_options: in_operator_optimization_options
- )
- end
+ let(:iterator) do
+ Gitlab::Pagination::Keyset::Iterator.new(
+ scope: scope.limit(batch_size),
+ in_operator_optimization_options: in_operator_optimization_options
+ )
+ end
+ shared_examples 'correct ordering examples' do |opts = {}|
let(:all_records) do
all_records = []
iterator.each_batch(of: batch_size) do |records|
@@ -49,8 +49,10 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
all_records
end
- it 'returns records in correct order' do
- expect(all_records).to eq(expected_order)
+ unless opts[:skip_finder_query_test]
+ it 'returns records in correct order' do
+ expect(all_records).to eq(expected_order)
+ end
end
context 'when not passing the finder query' do
@@ -248,4 +250,57 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
expect { described_class.new(**options).execute }.to raise_error(/The order on the scope does not support keyset pagination/)
end
+
+ context 'when ordering by SQL expression' do
+ let(:order) do
+ # ORDER BY (id * 10), id
+ Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id_multiplied_by_ten',
+ order_expression: Arel.sql('(id * 10)').asc,
+ sql_type: 'integer'
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Issue.arel_table[:id].asc
+ )
+ ])
+ end
+
+ let(:scope) { Issue.reorder(order) }
+ let(:expected_order) { issues.sort_by(&:id) }
+
+ let(:in_operator_optimization_options) do
+ {
+ array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id),
+ array_mapping_scope: -> (id_expression) { Issue.where(Issue.arel_table[:project_id].eq(id_expression)) }
+ }
+ end
+
+ context 'when iterating records one by one' do
+ let(:batch_size) { 1 }
+
+ it_behaves_like 'correct ordering examples', skip_finder_query_test: true
+ end
+
+ context 'when iterating records with LIMIT 3' do
+ let(:batch_size) { 3 }
+
+ it_behaves_like 'correct ordering examples', skip_finder_query_test: true
+ end
+
+ context 'when passing finder query' do
+ let(:batch_size) { 3 }
+
+ it 'raises error, loading complete rows are not supported with SQL expressions' do
+ in_operator_optimization_options[:finder_query] = -> (_, _) { Issue.select(:id, '(id * 10)').where(id: -1) }
+
+ expect(in_operator_optimization_options[:finder_query]).not_to receive(:call)
+
+ expect do
+ iterator.each_batch(of: batch_size) { |records| records.to_a }
+ end.to raise_error /The "RecordLoaderStrategy" does not support/
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb
index fe95d5406dd..ab1037b318b 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb
@@ -31,4 +31,41 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O
])
end
end
+
+ context 'when an SQL expression is given' do
+ context 'when the sql_type attribute is missing' do
+ let(:order) do
+ Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id_times_ten',
+ order_expression: Arel.sql('id * 10').asc
+ )
+ ])
+ end
+
+ let(:keyset_scope) { Project.order(order) }
+
+ it 'raises error' do
+ expect { strategy.initializer_columns }.to raise_error(Gitlab::Pagination::Keyset::SqlTypeMissingError)
+ end
+ end
+
+ context 'when the sql_type_attribute is present' do
+ let(:order) do
+ Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id_times_ten',
+ order_expression: Arel.sql('id * 10').asc,
+ sql_type: 'integer'
+ )
+ ])
+ end
+
+ let(:keyset_scope) { Project.order(order) }
+
+ it 'returns the initializer columns' do
+ expect(strategy.initializer_columns).to eq(['NULL::integer AS id_times_ten'])
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
deleted file mode 100644
index 76731bb916c..00000000000
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ /dev/null
@@ -1,676 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::MultiStore do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:redis_store_class) do
- Class.new(Gitlab::Redis::Wrapper) do
- def config_file_name
- config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
- Rails.root.join(config_file_name).to_s
- end
-
- def self.name
- 'Sessions'
- end
- end
- end
-
- let_it_be(:primary_db) { 1 }
- let_it_be(:secondary_db) { 2 }
- let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
- let_it_be(:instance_name) { 'TestStore' }
- let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- subject { multi_store.send(name, *args) }
-
- before do
- skip_feature_flags_yaml_validation
- skip_default_enabled_yaml_check
- end
-
- after(:all) do
- primary_store.flushdb
- secondary_store.flushdb
- end
-
- context 'when primary_store is nil' do
- let(:multi_store) { described_class.new(nil, secondary_store, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/)
- end
- end
-
- context 'when secondary_store is nil' do
- let(:multi_store) { described_class.new(primary_store, nil, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/)
- end
- end
-
- context 'when instance_name is nil' do
- let(:instance_name) { nil }
- let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /instance_name is required/)
- end
- end
-
- context 'when primary_store is not a ::Redis instance' do
- before do
- allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid primary_store/)
- end
- end
-
- context 'when secondary_store is not a ::Redis instance' do
- before do
- allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid secondary_store/)
- end
- end
-
- context 'with READ redis commands' do
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1"}
- let_it_be(:value2) { "redis_value2"}
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:keys) { [key1, key2] }
- let_it_be(:values) { [value1, value2] }
- let_it_be(:svalues) { [value2, value1] }
-
- where(:case_name, :name, :args, :value, :block) do
- 'execute :get command' | :get | ref(:key1) | ref(:value1) | nil
- 'execute :mget command' | :mget | ref(:keys) | ref(:values) | nil
- 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | ->(value) { value }
- 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | nil
- 'execute :scard command' | :scard | ref(:skey) | 2 | nil
- end
-
- before(:all) do
- primary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
-
- secondary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
- end
-
- RSpec.shared_examples_for 'reads correct value' do
- it 'returns the correct value' do
- if value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values (unsorted set)
- is_expected.to match_array(value)
- else
- is_expected.to eq(value)
- end
- end
- end
-
- RSpec.shared_examples_for 'fallback read from the secondary store' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- it 'fallback and execute on secondary instance' do
- expect(secondary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'logs the ReadFromPrimaryError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
- hash_including(command_name: name, extra: hash_including(instance_name: instance_name)))
-
- subject
- end
-
- it 'increment read fallback count metrics' do
- expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
-
- subject
- end
-
- include_examples 'reads correct value'
-
- context 'when fallback read from the secondary instance raises an exception' do
- before do
- allow(secondary_store).to receive(name).with(*args).and_raise(StandardError)
- end
-
- it 'fails with exception' do
- expect { subject }.to raise_error(StandardError)
- end
- end
- end
-
- RSpec.shared_examples_for 'secondary store' do
- it 'execute on the secondary instance' do
- expect(secondary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
-
- it 'does not execute on the primary store' do
- expect(primary_store).not_to receive(name)
-
- subject
- end
- end
-
- with_them do
- describe "#{name}" do
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when reading from the primary is successful' do
- it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
-
- include_examples 'reads correct value'
- end
-
- context 'when reading from primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name),
- command_name: name))
-
- subject
- end
-
- include_examples 'fallback read from the secondary store'
- end
-
- context 'when reading from primary instance return no value' do
- before do
- allow(primary_store).to receive(name).and_return(nil)
- end
-
- include_examples 'fallback read from the secondary store'
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on primary instance' do
- expect(primary_store).to receive(name).with(*args).once
-
- subject
- end
- end
-
- if params[:block]
- subject do
- multi_store.send(name, *args, &block)
- end
-
- context 'when block is provided' do
- it 'yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
-
- subject
- end
-
- include_examples 'reads correct value'
- end
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it_behaves_like 'secondary store'
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'execute on the primary instance' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
- end
- end
-
- context 'with both primary and secondary store using same redis instance' do
- let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- it_behaves_like 'secondary store'
- end
- end
- end
- end
-
- context 'with WRITE redis commands' do
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1"}
- let_it_be(:value2) { "redis_value2"}
- let_it_be(:key1_value1) { [key1, value1] }
- let_it_be(:key1_value2) { [key1, value2] }
- let_it_be(:ttl) { 10 }
- let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:svalues1) { [value2, value1] }
- let_it_be(:svalues2) { [value1] }
- let_it_be(:skey_value1) { [skey, value1] }
- let_it_be(:skey_value2) { [skey, value2] }
-
- where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
- 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
- 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
- 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
- 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
- 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
- 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
- end
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
-
- primary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
-
- secondary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
- end
-
- RSpec.shared_examples_for 'verify that store contains values' do |store|
- it "#{store} redis store contains correct values", :aggregate_errors do
- subject
-
- redis_store = multi_store.send(store)
-
- if expected_value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values
- expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
- else
- expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
- end
- end
- end
-
- with_them do
- describe "#{name}" do
- let(:expected_args) {args || no_args }
-
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when executing on primary instance is successful' do
- it 'executes on both primary and secondary redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when executing on the primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception and execute on secondary instance', :aggregate_errors do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(extra: hash_including(:multi_store_error_message), command_name: name))
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).once
- expect(secondary_store).to receive(name).with(*expected_args).once
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'executes only on the secondary redis store', :aggregate_errors do
- expect(secondary_store).to receive(name).with(*expected_args)
- expect(primary_store).not_to receive(name).with(*expected_args)
-
- subject
- end
-
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'executes only on the primary_redis redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args)
- expect(secondary_store).not_to receive(name).with(*expected_args)
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- end
- end
- end
- end
- end
-
- context 'with unsupported command' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- let_it_be(:key) { "redis:counter" }
-
- subject { multi_store.incr(key) }
-
- it 'executes method missing' do
- expect(multi_store).to receive(:method_missing)
-
- subject
- end
-
- context 'when command is not in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
- it 'logs MethodMissingError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError),
- hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)))
-
- subject
- end
-
- it 'increments method missing counter' do
- expect(counter).to receive(:increment).with(command: :incr, instance_name: instance_name)
-
- subject
- end
- end
-
- context 'when command is in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
- subject { multi_store.info }
-
- it 'does not log MethodMissingError' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- subject
- end
-
- it 'does not increment method missing counter' do
- expect(counter).not_to receive(:increment)
-
- subject
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'fallback and executes only on the secondary store', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).and_call_original
- expect(secondary_store).not_to receive(:incr)
-
- subject
- end
-
- it 'correct value is stored on the secondary store', :aggregate_errors do
- subject
-
- expect(secondary_store.get(key)).to be_nil
- expect(primary_store.get(key)).to eq('1')
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'fallback and executes only on the secondary store', :aggregate_errors do
- expect(secondary_store).to receive(:incr).with(key).and_call_original
- expect(primary_store).not_to receive(:incr)
-
- subject
- end
-
- it 'correct value is stored on the secondary store', :aggregate_errors do
- subject
-
- expect(primary_store.get(key)).to be_nil
- expect(secondary_store.get(key)).to eq('1')
- end
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.incr(key)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).once
- expect(secondary_store).to receive(:incr).with(key).once
-
- subject
- end
-
- it "both redis stores are containing correct values", :aggregate_errors do
- subject
-
- expect(primary_store.get(key)).to eq('1')
- expect(secondary_store.get(key)).to eq('1')
- end
- end
- end
-
- describe '#to_s' do
- subject { multi_store.to_s }
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(secondary_store.to_s)
- end
- end
- end
- end
-
- describe '#is_a?' do
- it 'returns true for ::Redis::Store' do
- expect(multi_store.is_a?(::Redis::Store)).to be true
- end
- end
-
- describe '#use_primary_and_secondary_stores?' do
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
- end
-
- describe '#use_primary_store_as_default?' do
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be true
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be false
- end
- end
- end
-
- def create_redis_store(options, extras = {})
- ::Redis::Store.new(options.merge(extras))
- end
-end
diff --git a/spec/lib/gitlab/redis/sessions_spec.rb b/spec/lib/gitlab/redis/sessions_spec.rb
index 6ecbbf3294d..b02864cb73d 100644
--- a/spec/lib/gitlab/redis/sessions_spec.rb
+++ b/spec/lib/gitlab/redis/sessions_spec.rb
@@ -6,31 +6,16 @@ RSpec.describe Gitlab::Redis::Sessions do
it_behaves_like "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
describe 'redis instance used in connection pool' do
- before do
+ around do |example|
clear_pool
- end
-
- after do
+ example.run
+ ensure
clear_pool
end
- context 'when redis.sessions configuration is not provided' do
- it 'uses ::Redis instance' do
- expect(described_class).to receive(:config_fallback?).and_return(true)
-
- described_class.pool.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Redis)
- end
- end
- end
-
- context 'when redis.sessions configuration is provided' do
- it 'instantiates an instance of MultiStore' do
- expect(described_class).to receive(:config_fallback?).and_return(false)
-
- described_class.pool.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
- end
+ it 'uses ::Redis instance' do
+ described_class.pool.with do |redis_instance|
+ expect(redis_instance).to be_instance_of(::Redis)
end
end
@@ -44,49 +29,9 @@ RSpec.describe Gitlab::Redis::Sessions do
describe '#store' do
subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
- context 'when redis.sessions configuration is NOT provided' do
- it 'instantiates ::Redis instance' do
- expect(described_class).to receive(:config_fallback?).and_return(true)
- expect(store).to be_instance_of(::Redis::Store)
- end
- end
-
- context 'when redis.sessions configuration is provided' do
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
-
- before do
- redis_clear_raw_config!(Gitlab::Redis::Sessions)
- redis_clear_raw_config!(Gitlab::Redis::SharedState)
- allow(described_class).to receive(:config_fallback?).and_return(false)
- end
-
- after do
- redis_clear_raw_config!(Gitlab::Redis::Sessions)
- redis_clear_raw_config!(Gitlab::Redis::SharedState)
- end
-
- # Check that Gitlab::Redis::Sessions is configured as MultiStore with proper attrs.
- it 'instantiates an instance of MultiStore', :aggregate_failures do
- expect(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- expect(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
-
- expect(store).to be_instance_of(::Gitlab::Redis::MultiStore)
-
- expect(store.primary_store.to_s).to eq("Redis Client connected to test-host:6379 against DB 99 with namespace session:gitlab")
- expect(store.secondary_store.to_s).to eq("Redis Client connected to /path/to/redis.sock against DB 0 with namespace session:gitlab")
-
- expect(store.instance_name).to eq('Sessions')
- end
-
- context 'when MultiStore correctly configured' do
- before do
- allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- allow(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
- end
-
- it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sessions, :use_primary_store_as_default_for_sessions
- end
+ # Check that Gitlab::Redis::Sessions is configured as RedisStore.
+ it 'instantiates an instance of Redis::Store' do
+ expect(store).to be_instance_of(::Redis::Store)
end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 83f85cc73d0..8d67350f0f3 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -433,6 +433,7 @@ RSpec.describe Gitlab::Regex do
describe '.nuget_version_regex' do
subject { described_class.nuget_version_regex }
+ it { is_expected.to match('1.2') }
it { is_expected.to match('1.2.3') }
it { is_expected.to match('1.2.3.4') }
it { is_expected.to match('1.2.3.4-stable.1') }
@@ -440,7 +441,6 @@ RSpec.describe Gitlab::Regex do
it { is_expected.to match('1.2.3-alpha.3') }
it { is_expected.to match('1.0.7+r3456') }
it { is_expected.not_to match('1') }
- it { is_expected.not_to match('1.2') }
it { is_expected.not_to match('1./2.3') }
it { is_expected.not_to match('../../../../../1.2.3') }
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
diff --git a/spec/lib/gitlab/search/params_spec.rb b/spec/lib/gitlab/search/params_spec.rb
index 6d15337b872..13770e550ec 100644
--- a/spec/lib/gitlab/search/params_spec.rb
+++ b/spec/lib/gitlab/search/params_spec.rb
@@ -133,4 +133,12 @@ RSpec.describe Gitlab::Search::Params do
end
end
end
+
+ describe '#email_lookup?' do
+ it 'is true if at least 1 word in search is an email' do
+ expect(described_class.new({ search: 'email@example.com' })).to be_email_lookup
+ expect(described_class.new({ search: 'foo email@example.com bar' })).to be_email_lookup
+ expect(described_class.new({ search: 'foo bar' })).not_to be_email_lookup
+ end
+ end
end
diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb
index 5c47ac7e9a0..0c25cc7dab5 100644
--- a/spec/lib/gitlab/shard_health_cache_spec.rb
+++ b/spec/lib/gitlab/shard_health_cache_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
let(:shards) { %w(foo bar) }
before do
- described_class.update(shards)
+ described_class.update(shards) # rubocop:disable Rails/SaveBang
end
describe '.clear' do
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
it 'replaces the existing set' do
new_set = %w(test me more)
- described_class.update(new_set)
+ described_class.update(new_set) # rubocop:disable Rails/SaveBang
expect(described_class.cached_healthy_shards).to match_array(new_set)
end
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
end
it 'returns 0 if no shards are available' do
- described_class.update([])
+ described_class.update([]) # rubocop:disable Rails/SaveBang
expect(described_class.healthy_shard_count).to eq(0)
end
diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb
deleted file mode 100644
index fcf8e6638f8..00000000000
--- a/spec/lib/gitlab/sherlock/collection_spec.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::Collection do
- let(:collection) { described_class.new }
-
- let(:transaction) do
- Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures')
- end
-
- describe '#add' do
- it 'adds a new transaction' do
- collection.add(transaction)
-
- expect(collection).not_to be_empty
- end
-
- it 'is aliased as <<' do
- collection << transaction
-
- expect(collection).not_to be_empty
- end
- end
-
- describe '#each' do
- it 'iterates over every transaction' do
- collection.add(transaction)
-
- expect { |b| collection.each(&b) }.to yield_with_args(transaction)
- end
- end
-
- describe '#clear' do
- it 'removes all transactions' do
- collection.add(transaction)
-
- collection.clear
-
- expect(collection).to be_empty
- end
- end
-
- describe '#empty?' do
- it 'returns true for an empty collection' do
- expect(collection).to be_empty
- end
-
- it 'returns false for a collection with a transaction' do
- collection.add(transaction)
-
- expect(collection).not_to be_empty
- end
- end
-
- describe '#find_transaction' do
- it 'returns the transaction for the given ID' do
- collection.add(transaction)
-
- expect(collection.find_transaction(transaction.id)).to eq(transaction)
- end
-
- it 'returns nil when no transaction could be found' do
- collection.add(transaction)
-
- expect(collection.find_transaction('cats')).to be_nil
- end
- end
-
- describe '#newest_first' do
- it 'returns transactions sorted from new to old' do
- trans1 = Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures')
- trans2 = Gitlab::Sherlock::Transaction.new('POST', '/more_cat_pictures')
-
- allow(trans1).to receive(:finished_at).and_return(Time.utc(2015, 1, 1))
- allow(trans2).to receive(:finished_at).and_return(Time.utc(2015, 1, 2))
-
- collection.add(trans1)
- collection.add(trans2)
-
- expect(collection.newest_first).to eq([trans2, trans1])
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/file_sample_spec.rb b/spec/lib/gitlab/sherlock/file_sample_spec.rb
deleted file mode 100644
index 8a1aa51e2d4..00000000000
--- a/spec/lib/gitlab/sherlock/file_sample_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::FileSample do
- let(:sample) { described_class.new(__FILE__, [], 150.4, 2) }
-
- describe '#id' do
- it 'returns the ID' do
- expect(sample.id).to be_an_instance_of(String)
- end
- end
-
- describe '#file' do
- it 'returns the file path' do
- expect(sample.file).to eq(__FILE__)
- end
- end
-
- describe '#line_samples' do
- it 'returns the line samples' do
- expect(sample.line_samples).to eq([])
- end
- end
-
- describe '#events' do
- it 'returns the total number of events' do
- expect(sample.events).to eq(2)
- end
- end
-
- describe '#duration' do
- it 'returns the total execution time' do
- expect(sample.duration).to eq(150.4)
- end
- end
-
- describe '#relative_path' do
- it 'returns the relative path' do
- expect(sample.relative_path)
- .to eq('spec/lib/gitlab/sherlock/file_sample_spec.rb')
- end
- end
-
- describe '#to_param' do
- it 'returns the sample ID' do
- expect(sample.to_param).to eq(sample.id)
- end
- end
-
- describe '#source' do
- it 'returns the contents of the file' do
- expect(sample.source).to eq(File.read(__FILE__))
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/line_profiler_spec.rb b/spec/lib/gitlab/sherlock/line_profiler_spec.rb
deleted file mode 100644
index 2220a2cafc8..00000000000
--- a/spec/lib/gitlab/sherlock/line_profiler_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::LineProfiler do
- let(:profiler) { described_class.new }
-
- describe '#profile' do
- it 'runs the profiler when using MRI' do
- allow(profiler).to receive(:mri?).and_return(true)
- allow(profiler).to receive(:profile_mri)
-
- profiler.profile { 'cats' }
- end
-
- it 'raises NotImplementedError when profiling an unsupported platform' do
- allow(profiler).to receive(:mri?).and_return(false)
-
- expect { profiler.profile { 'cats' } }.to raise_error(NotImplementedError)
- end
- end
-
- describe '#profile_mri' do
- it 'returns an Array containing the return value and profiling samples' do
- allow(profiler).to receive(:lineprof)
- .and_yield
- .and_return({ __FILE__ => [[0, 0, 0, 0]] })
-
- retval, samples = profiler.profile_mri { 42 }
-
- expect(retval).to eq(42)
- expect(samples).to eq([])
- end
- end
-
- describe '#aggregate_rblineprof' do
- let(:raw_samples) do
- { __FILE__ => [[30000, 30000, 5, 0], [15000, 15000, 4, 0]] }
- end
-
- it 'returns an Array of FileSample objects' do
- samples = profiler.aggregate_rblineprof(raw_samples)
-
- expect(samples).to be_an_instance_of(Array)
- expect(samples[0]).to be_an_instance_of(Gitlab::Sherlock::FileSample)
- end
-
- describe 'the first FileSample object' do
- let(:file_sample) do
- profiler.aggregate_rblineprof(raw_samples)[0]
- end
-
- it 'uses the correct file path' do
- expect(file_sample.file).to eq(__FILE__)
- end
-
- it 'contains a list of line samples' do
- line_sample = file_sample.line_samples[0]
-
- expect(line_sample).to be_an_instance_of(Gitlab::Sherlock::LineSample)
-
- expect(line_sample.duration).to eq(15.0)
- expect(line_sample.events).to eq(4)
- end
-
- it 'contains the total file execution time' do
- expect(file_sample.duration).to eq(30.0)
- end
-
- it 'contains the total amount of file events' do
- expect(file_sample.events).to eq(5)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/line_sample_spec.rb b/spec/lib/gitlab/sherlock/line_sample_spec.rb
deleted file mode 100644
index db031377787..00000000000
--- a/spec/lib/gitlab/sherlock/line_sample_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::LineSample do
- let(:sample) { described_class.new(150.0, 4) }
-
- describe '#duration' do
- it 'returns the duration' do
- expect(sample.duration).to eq(150.0)
- end
- end
-
- describe '#events' do
- it 'returns the amount of events' do
- expect(sample.events).to eq(4)
- end
- end
-
- describe '#percentage_of' do
- it 'returns the percentage of 1500.0' do
- expect(sample.percentage_of(1500.0)).to be_within(0.1).of(10.0)
- end
- end
-
- describe '#majority_of' do
- it 'returns true if the sample takes up the majority of the given duration' do
- expect(sample.majority_of?(500.0)).to eq(true)
- end
-
- it "returns false if the sample doesn't take up the majority of the given duration" do
- expect(sample.majority_of?(1500.0)).to eq(false)
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/location_spec.rb b/spec/lib/gitlab/sherlock/location_spec.rb
deleted file mode 100644
index 4a8b5dffba2..00000000000
--- a/spec/lib/gitlab/sherlock/location_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::Location do
- let(:location) { described_class.new(__FILE__, 1) }
-
- describe 'from_ruby_location' do
- it 'creates a Location from a Thread::Backtrace::Location' do
- input = caller_locations[0]
- output = described_class.from_ruby_location(input)
-
- expect(output).to be_an_instance_of(described_class)
- expect(output.path).to eq(input.path)
- expect(output.line).to eq(input.lineno)
- end
- end
-
- describe '#path' do
- it 'returns the file path' do
- expect(location.path).to eq(__FILE__)
- end
- end
-
- describe '#line' do
- it 'returns the line number' do
- expect(location.line).to eq(1)
- end
- end
-
- describe '#application?' do
- it 'returns true for an application frame' do
- expect(location.application?).to eq(true)
- end
-
- it 'returns false for a non application frame' do
- loc = described_class.new('/tmp/cats.rb', 1)
-
- expect(loc.application?).to eq(false)
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/middleware_spec.rb b/spec/lib/gitlab/sherlock/middleware_spec.rb
deleted file mode 100644
index 645bde6681d..00000000000
--- a/spec/lib/gitlab/sherlock/middleware_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::Middleware do
- let(:app) { double(:app) }
- let(:middleware) { described_class.new(app) }
-
- describe '#call' do
- describe 'when instrumentation is enabled' do
- it 'instruments a request' do
- allow(middleware).to receive(:instrument?).and_return(true)
- allow(middleware).to receive(:call_with_instrumentation)
-
- middleware.call({})
- end
- end
-
- describe 'when instrumentation is disabled' do
- it "doesn't instrument a request" do
- allow(middleware).to receive(:instrument).and_return(false)
- allow(app).to receive(:call)
-
- middleware.call({})
- end
- end
- end
-
- describe '#call_with_instrumentation' do
- it 'instruments a request' do
- trans = double(:transaction)
- retval = 'cats are amazing'
- env = {}
-
- allow(app).to receive(:call).with(env).and_return(retval)
- allow(middleware).to receive(:transaction_from_env).and_return(trans)
- allow(trans).to receive(:run).and_yield.and_return(retval)
- allow(Gitlab::Sherlock.collection).to receive(:add).with(trans)
-
- middleware.call_with_instrumentation(env)
- end
- end
-
- describe '#instrument?' do
- it 'returns false for a text/css request' do
- env = { 'HTTP_ACCEPT' => 'text/css', 'REQUEST_URI' => '/' }
-
- expect(middleware.instrument?(env)).to eq(false)
- end
-
- it 'returns false for a request to a Sherlock route' do
- env = {
- 'HTTP_ACCEPT' => 'text/html',
- 'REQUEST_URI' => '/sherlock/transactions'
- }
-
- expect(middleware.instrument?(env)).to eq(false)
- end
-
- it 'returns true for a request that should be instrumented' do
- env = {
- 'HTTP_ACCEPT' => 'text/html',
- 'REQUEST_URI' => '/cats'
- }
-
- expect(middleware.instrument?(env)).to eq(true)
- end
- end
-
- describe '#transaction_from_env' do
- it 'returns a Transaction' do
- env = {
- 'HTTP_ACCEPT' => 'text/html',
- 'REQUEST_URI' => '/cats'
- }
-
- expect(middleware.transaction_from_env(env))
- .to be_an_instance_of(Gitlab::Sherlock::Transaction)
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb
deleted file mode 100644
index b8dfd082c37..00000000000
--- a/spec/lib/gitlab/sherlock/query_spec.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::Query do
- let(:started_at) { Time.utc(2015, 1, 1) }
- let(:finished_at) { started_at + 5 }
-
- let(:query) do
- described_class.new('SELECT COUNT(*) FROM users', started_at, finished_at)
- end
-
- describe 'new_with_bindings' do
- it 'returns a Query' do
- sql = 'SELECT COUNT(*) FROM users WHERE id = $1'
- bindings = [[double(:column), 10]]
-
- query = described_class
- .new_with_bindings(sql, bindings, started_at, finished_at)
-
- expect(query.query).to eq('SELECT COUNT(*) FROM users WHERE id = 10;')
- end
- end
-
- describe '#id' do
- it 'returns a String' do
- expect(query.id).to be_an_instance_of(String)
- end
- end
-
- describe '#query' do
- it 'returns the query with a trailing semi-colon' do
- expect(query.query).to eq('SELECT COUNT(*) FROM users;')
- end
- end
-
- describe '#started_at' do
- it 'returns the start time' do
- expect(query.started_at).to eq(started_at)
- end
- end
-
- describe '#finished_at' do
- it 'returns the completion time' do
- expect(query.finished_at).to eq(finished_at)
- end
- end
-
- describe '#backtrace' do
- it 'returns the backtrace' do
- expect(query.backtrace).to be_an_instance_of(Array)
- end
- end
-
- describe '#duration' do
- it 'returns the duration in milliseconds' do
- expect(query.duration).to be_within(0.1).of(5000.0)
- end
- end
-
- describe '#to_param' do
- it 'returns the query ID' do
- expect(query.to_param).to eq(query.id)
- end
- end
-
- describe '#formatted_query' do
- it 'returns a formatted version of the query' do
- expect(query.formatted_query).to eq(<<-EOF.strip)
-SELECT COUNT(*)
-FROM users;
- EOF
- end
- end
-
- describe '#last_application_frame' do
- it 'returns the last application frame' do
- frame = query.last_application_frame
-
- expect(frame).to be_an_instance_of(Gitlab::Sherlock::Location)
- expect(frame.path).to eq(__FILE__)
- end
- end
-
- describe '#application_backtrace' do
- it 'returns an Array of application frames' do
- frames = query.application_backtrace
-
- expect(frames).to be_an_instance_of(Array)
- expect(frames).not_to be_empty
-
- frames.each do |frame|
- expect(frame.path).to start_with(Rails.root.to_s)
- end
- end
- end
-
- describe '#explain' do
- it 'returns the query plan as a String' do
- lines = [
- ['Aggregate (cost=123 rows=1)'],
- [' -> Index Only Scan using index_cats_are_amazing']
- ]
-
- result = double(:result, values: lines)
-
- allow(query).to receive(:raw_explain).and_return(result)
-
- expect(query.explain).to eq(<<-EOF.strip)
-Aggregate (cost=123 rows=1)
- -> Index Only Scan using index_cats_are_amazing
- EOF
- end
- end
-end
diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb
deleted file mode 100644
index 535b0ad4d8a..00000000000
--- a/spec/lib/gitlab/sherlock/transaction_spec.rb
+++ /dev/null
@@ -1,238 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Sherlock::Transaction do
- let(:transaction) { described_class.new('POST', '/cat_pictures') }
-
- describe '#id' do
- it 'returns the transaction ID' do
- expect(transaction.id).to be_an_instance_of(String)
- end
- end
-
- describe '#type' do
- it 'returns the type' do
- expect(transaction.type).to eq('POST')
- end
- end
-
- describe '#path' do
- it 'returns the path' do
- expect(transaction.path).to eq('/cat_pictures')
- end
- end
-
- describe '#queries' do
- it 'returns an Array of queries' do
- expect(transaction.queries).to be_an_instance_of(Array)
- end
- end
-
- describe '#file_samples' do
- it 'returns an Array of file samples' do
- expect(transaction.file_samples).to be_an_instance_of(Array)
- end
- end
-
- describe '#started_at' do
- it 'returns the start time' do
- allow(transaction).to receive(:profile_lines).and_yield
-
- transaction.run { 'cats are amazing' }
-
- expect(transaction.started_at).to be_an_instance_of(Time)
- end
- end
-
- describe '#finished_at' do
- it 'returns the completion time' do
- allow(transaction).to receive(:profile_lines).and_yield
-
- transaction.run { 'cats are amazing' }
-
- expect(transaction.finished_at).to be_an_instance_of(Time)
- end
- end
-
- describe '#view_counts' do
- it 'returns a Hash' do
- expect(transaction.view_counts).to be_an_instance_of(Hash)
- end
-
- it 'sets the default value of a key to 0' do
- expect(transaction.view_counts['cats.rb']).to be_zero
- end
- end
-
- describe '#run' do
- it 'runs the transaction' do
- allow(transaction).to receive(:profile_lines).and_yield
-
- retval = transaction.run { 'cats are amazing' }
-
- expect(retval).to eq('cats are amazing')
- end
- end
-
- describe '#duration' do
- it 'returns the duration in seconds' do
- start_time = Time.now
-
- allow(transaction).to receive(:started_at).and_return(start_time)
- allow(transaction).to receive(:finished_at).and_return(start_time + 5)
-
- expect(transaction.duration).to be_within(0.1).of(5.0)
- end
- end
-
- describe '#query_duration' do
- it 'returns the total query duration in seconds' do
- time = Time.now
- query1 = Gitlab::Sherlock::Query.new('SELECT 1', time, time + 5)
- query2 = Gitlab::Sherlock::Query.new('SELECT 2', time, time + 2)
-
- transaction.queries << query1
- transaction.queries << query2
-
- expect(transaction.query_duration).to be_within(0.1).of(7.0)
- end
- end
-
- describe '#to_param' do
- it 'returns the transaction ID' do
- expect(transaction.to_param).to eq(transaction.id)
- end
- end
-
- describe '#sorted_queries' do
- it 'returns the queries in descending order' do
- start_time = Time.now
-
- query1 = Gitlab::Sherlock::Query.new('SELECT 1', start_time, start_time)
-
- query2 = Gitlab::Sherlock::Query
- .new('SELECT 2', start_time, start_time + 5)
-
- transaction.queries << query1
- transaction.queries << query2
-
- expect(transaction.sorted_queries).to eq([query2, query1])
- end
- end
-
- describe '#sorted_file_samples' do
- it 'returns the file samples in descending order' do
- sample1 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1)
- sample2 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 15.0, 1)
-
- transaction.file_samples << sample1
- transaction.file_samples << sample2
-
- expect(transaction.sorted_file_samples).to eq([sample2, sample1])
- end
- end
-
- describe '#find_query' do
- it 'returns a Query when found' do
- query = Gitlab::Sherlock::Query.new('SELECT 1', Time.now, Time.now)
-
- transaction.queries << query
-
- expect(transaction.find_query(query.id)).to eq(query)
- end
-
- it 'returns nil when no query could be found' do
- expect(transaction.find_query('cats')).to be_nil
- end
- end
-
- describe '#find_file_sample' do
- it 'returns a FileSample when found' do
- sample = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1)
-
- transaction.file_samples << sample
-
- expect(transaction.find_file_sample(sample.id)).to eq(sample)
- end
-
- it 'returns nil when no file sample could be found' do
- expect(transaction.find_file_sample('cats')).to be_nil
- end
- end
-
- describe '#profile_lines' do
- describe 'when line profiling is enabled' do
- it 'yields the block using the line profiler' do
- allow(Gitlab::Sherlock).to receive(:enable_line_profiler?)
- .and_return(true)
-
- allow_next_instance_of(Gitlab::Sherlock::LineProfiler) do |instance|
- allow(instance).to receive(:profile).and_return('cats are amazing', [])
- end
-
- retval = transaction.profile_lines { 'cats are amazing' }
-
- expect(retval).to eq('cats are amazing')
- end
- end
-
- describe 'when line profiling is disabled' do
- it 'yields the block' do
- allow(Gitlab::Sherlock).to receive(:enable_line_profiler?)
- .and_return(false)
-
- retval = transaction.profile_lines { 'cats are amazing' }
-
- expect(retval).to eq('cats are amazing')
- end
- end
- end
-
- describe '#subscribe_to_active_record' do
- let(:subscription) { transaction.subscribe_to_active_record }
- let(:time) { Time.now }
- let(:query_data) { { sql: 'SELECT 1', binds: [] } }
-
- after do
- ActiveSupport::Notifications.unsubscribe(subscription)
- end
-
- it 'tracks executed queries' do
- expect(transaction).to receive(:track_query)
- .with('SELECT 1', [], time, time)
-
- subscription.publish('test', time, time, nil, query_data)
- end
-
- it 'only tracks queries triggered from the transaction thread' do
- expect(transaction).not_to receive(:track_query)
-
- Thread.new { subscription.publish('test', time, time, nil, query_data) }
- .join
- end
- end
-
- describe '#subscribe_to_action_view' do
- let(:subscription) { transaction.subscribe_to_action_view }
- let(:time) { Time.now }
- let(:view_data) { { identifier: 'foo.rb' } }
-
- after do
- ActiveSupport::Notifications.unsubscribe(subscription)
- end
-
- it 'tracks rendered views' do
- expect(transaction).to receive(:track_view).with('foo.rb')
-
- subscription.publish('test', time, time, nil, view_data)
- end
-
- it 'only tracks views rendered from the transaction thread' do
- expect(transaction).not_to receive(:track_view)
-
- Thread.new { subscription.publish('test', time, time, nil, view_data) }
- .join
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
index 2f2499753b9..9affc3d5146 100644
--- a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
@@ -2,11 +2,11 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::SidekiqStatus::ClientMiddleware do
+RSpec.describe Gitlab::SidekiqStatus::ClientMiddleware, :clean_gitlab_redis_queues do
describe '#call' do
context 'when the job has status_expiration set' do
- it 'tracks the job in Redis with a value of 2' do
- expect(Gitlab::SidekiqStatus).to receive(:set).with('123', 1.hour.to_i, value: 2)
+ it 'tracks the job in Redis' do
+ expect(Gitlab::SidekiqStatus).to receive(:set).with('123', 1.hour.to_i)
described_class.new
.call('Foo', { 'jid' => '123', 'status_expiration' => 1.hour.to_i }, double(:queue), double(:pool)) { nil }
@@ -14,8 +14,8 @@ RSpec.describe Gitlab::SidekiqStatus::ClientMiddleware do
end
context 'when the job does not have status_expiration set' do
- it 'tracks the job in Redis with a value of 1' do
- expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION, value: 1)
+ it 'does not track the job in Redis' do
+ expect(Gitlab::SidekiqStatus).to receive(:set).with('123', nil)
described_class.new
.call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil }
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index 1e7b52471b0..c94deb8e008 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
Sidekiq.redis do |redis|
expect(redis.exists(key)).to eq(true)
expect(redis.ttl(key) > 0).to eq(true)
- expect(redis.get(key)).to eq(described_class::DEFAULT_VALUE.to_s)
+ expect(redis.get(key)).to eq('1')
end
end
@@ -24,19 +24,17 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
Sidekiq.redis do |redis|
expect(redis.exists(key)).to eq(true)
expect(redis.ttl(key) > described_class::DEFAULT_EXPIRATION).to eq(true)
- expect(redis.get(key)).to eq(described_class::DEFAULT_VALUE.to_s)
+ expect(redis.get(key)).to eq('1')
end
end
- it 'allows overriding the default value' do
- described_class.set('123', value: 2)
+ it 'does not store anything with a nil expiry' do
+ described_class.set('123', nil)
key = described_class.key_for('123')
Sidekiq.redis do |redis|
- expect(redis.exists(key)).to eq(true)
- expect(redis.ttl(key) > 0).to eq(true)
- expect(redis.get(key)).to eq('2')
+ expect(redis.exists(key)).to eq(false)
end
end
end
@@ -138,33 +136,5 @@ RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_
it 'handles an empty array' do
expect(described_class.job_status([])).to eq([])
end
-
- context 'when log_implicit_sidekiq_status_calls is enabled' do
- it 'logs keys that contained the default value' do
- described_class.set('123', value: 2)
- described_class.set('456')
- described_class.set('012')
-
- expect(Sidekiq.logger).to receive(:info).with(message: described_class::DEFAULT_VALUE_MESSAGE,
- keys: [described_class.key_for('456'), described_class.key_for('012')])
-
- expect(described_class.job_status(%w(123 456 789 012))).to eq([true, true, false, true])
- end
- end
-
- context 'when log_implicit_sidekiq_status_calls is disabled' do
- before do
- stub_feature_flags(log_implicit_sidekiq_status_calls: false)
- end
-
- it 'does not perform any logging' do
- described_class.set('123', value: 2)
- described_class.set('456')
-
- expect(Sidekiq.logger).not_to receive(:info)
-
- expect(described_class.job_status(%w(123 456 789))).to eq([true, true, false])
- end
- end
end
end
diff --git a/spec/lib/gitlab/sourcegraph_spec.rb b/spec/lib/gitlab/sourcegraph_spec.rb
index 6bebd1ca3e6..e2c1e959cbf 100644
--- a/spec/lib/gitlab/sourcegraph_spec.rb
+++ b/spec/lib/gitlab/sourcegraph_spec.rb
@@ -37,6 +37,12 @@ RSpec.describe Gitlab::Sourcegraph do
it { is_expected.to be_truthy }
end
+
+ context 'when feature is disabled' do
+ let(:feature_scope) { false }
+
+ it { is_expected.to be_falsey }
+ end
end
describe '.feature_enabled?' do
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index e1a588a4b7d..38486b313cb 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -21,6 +21,14 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
end
end
+ describe '.supported_types' do
+ it 'returns array with the names of supported technologies' do
+ expect(described_class.supported_types).to eq(
+ [:rsa, :dsa, :ecdsa, :ed25519]
+ )
+ end
+ end
+
describe '.supported_sizes(name)' do
where(:name, :sizes) do
[
@@ -31,14 +39,43 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true do
]
end
- subject { described_class.supported_sizes(name) }
-
with_them do
it { expect(described_class.supported_sizes(name)).to eq(sizes) }
it { expect(described_class.supported_sizes(name.to_s)).to eq(sizes) }
end
end
+ describe '.supported_algorithms' do
+ it 'returns all supported algorithms' do
+ expect(described_class.supported_algorithms).to eq(
+ %w(
+ ssh-rsa
+ ssh-dss
+ ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521
+ ssh-ed25519
+ )
+ )
+ end
+ end
+
+ describe '.supported_algorithms_for_name' do
+ where(:name, :algorithms) do
+ [
+ [:rsa, %w(ssh-rsa)],
+ [:dsa, %w(ssh-dss)],
+ [:ecdsa, %w(ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521)],
+ [:ed25519, %w(ssh-ed25519)]
+ ]
+ end
+
+ with_them do
+ it "returns all supported algorithms for #{params[:name]}" do
+ expect(described_class.supported_algorithms_for_name(name)).to eq(algorithms)
+ expect(described_class.supported_algorithms_for_name(name.to_s)).to eq(algorithms)
+ end
+ end
+ end
+
describe '.sanitize(key_content)' do
let(:content) { build(:key).key }
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
index 6d03cf496b8..c9dc23d7c14 100644
--- a/spec/lib/gitlab/themes_spec.rb
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Themes, lib: true do
it 'prevents an infinite loop when configuration default is invalid' do
default = described_class::APPLICATION_DEFAULT
- themes = described_class::THEMES
+ themes = described_class.available_themes
config = double(default_theme: 0).as_null_object
allow(Gitlab).to receive(:config).and_return(config)
diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb
index 7d678db5ec8..c88b0af30f6 100644
--- a/spec/lib/gitlab/tracking/standard_context_spec.rb
+++ b/spec/lib/gitlab/tracking/standard_context_spec.rb
@@ -58,6 +58,10 @@ RSpec.describe Gitlab::Tracking::StandardContext do
expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE)
end
+ it 'contains context_generated_at timestamp', :freeze_time do
+ expect(snowplow_context.to_json.dig(:data, :context_generated_at)).to eq(Time.current)
+ end
+
context 'plan' do
context 'when namespace is not available' do
it 'is nil' do
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index 0a32bdb95d3..4d84423cde4 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
let_it_be(:issues) { Issue.all }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(Issue.connection).to receive(:transaction_open?).and_return(false)
end
it 'calculates a correct result' do
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
end.new(time_frame: 'all')
end
- it 'calculates a correct result' do
+ it 'calculates a correct result', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/349762' do
expect(subject.value).to be_within(Gitlab::Database::PostgresHll::BatchDistinctCounter::ERROR_RATE).percent_of(3)
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
index c8cb1bb4373..cc4df696b37 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
@@ -17,9 +17,25 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
end
context 'when raising an exception' do
- it 'return the custom fallback' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(should_raise_for_dev)
expect(ApplicationRecord.database).to receive(:version).and_raise('Error')
- expect(subject.value).to eq(custom_fallback)
+ end
+
+ context 'with should_raise_for_dev? false' do
+ let(:should_raise_for_dev) { false }
+
+ it 'return the custom fallback' do
+ expect(subject.value).to eq(custom_fallback)
+ end
+ end
+
+ context 'with should_raise_for_dev? true' do
+ let(:should_raise_for_dev) { true }
+
+ it 'raises an error' do
+ expect { subject.value }.to raise_error('Error')
+ end
end
end
end
@@ -38,9 +54,25 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
end
context 'when raising an exception' do
- it 'return the default fallback' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(should_raise_for_dev)
expect(ApplicationRecord.database).to receive(:version).and_raise('Error')
- expect(subject.value).to eq(described_class::FALLBACK)
+ end
+
+ context 'with should_raise_for_dev? false' do
+ let(:should_raise_for_dev) { false }
+
+ it 'return the default fallback' do
+ expect(subject.value).to eq(described_class::FALLBACK)
+ end
+ end
+
+ context 'with should_raise_for_dev? true' do
+ let(:should_raise_for_dev) { true }
+
+ it 'raises an error' do
+ expect { subject.value }.to raise_error('Error')
+ end
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index 0ec805714e3..f7ff68af8a2 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -48,7 +48,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'epic_boards_usage',
'secure',
'importer',
- 'network_policies'
+ 'network_policies',
+ 'geo'
)
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb
index 6f201b43390..1ac344d9250 100644
--- a/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb
@@ -13,10 +13,6 @@ RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_red
end
end
- it 'includes the right events' do
- expect(described_class::KNOWN_EVENTS.size).to eq 63
- end
-
described_class::KNOWN_EVENTS.each do |event|
it_behaves_like 'usage counter with totals', event
end
@@ -24,8 +20,8 @@ RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_red
describe '.fetch_supported_event' do
subject { described_class.fetch_supported_event(event_name) }
- let(:event_name) { 'package_events_i_package_composer_push_package' }
+ let(:event_name) { 'package_events_i_package_conan_push_package' }
- it { is_expected.to eq 'i_package_composer_push_package' }
+ it { is_expected.to eq 'i_package_conan_push_package' }
end
end
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 64eff76a9f2..a8cf87d9364 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -3,10 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageDataQueries do
- before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
- end
-
describe '#add_metric' do
let(:metric) { 'CountBoardsMetric' }
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 015ecd1671e..427e8e67090 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
stub_usage_data_connections
stub_object_store_settings
clear_memoized_values(described_class::CE_MEMOIZED_VALUES)
+ stub_database_flavor_check('Cloud SQL for PostgreSQL')
end
describe '.uncached_data' do
@@ -160,7 +161,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
another_project = create(:project, :repository, creator: another_user)
create(:remote_mirror, project: another_project, enabled: false)
create(:snippet, author: user)
- create(:suggestion, note: create(:note, project: project))
end
expect(described_class.usage_activity_by_stage_create({})).to include(
@@ -170,8 +170,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_with_disable_overriding_approvers_per_merge_request: 2,
projects_without_disable_overriding_approvers_per_merge_request: 6,
remote_mirrors: 2,
- snippets: 2,
- suggestions: 2
+ snippets: 2
)
expect(described_class.usage_activity_by_stage_create(described_class.monthly_time_range_db_params)).to include(
deploy_keys: 1,
@@ -180,8 +179,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_with_disable_overriding_approvers_per_merge_request: 1,
projects_without_disable_overriding_approvers_per_merge_request: 3,
remote_mirrors: 1,
- snippets: 1,
- suggestions: 1
+ snippets: 1
)
end
end
@@ -278,8 +276,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(described_class.usage_activity_by_stage_manage({})).to include(
{
bulk_imports: {
- gitlab_v1: 2,
- gitlab: Gitlab::UsageData::DEPRECATED_VALUE
+ gitlab_v1: 2
},
project_imports: {
bitbucket: 2,
@@ -302,32 +299,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
group_imports: {
group_import: 2,
gitlab_migration: 2
- },
- projects_imported: {
- total: Gitlab::UsageData::DEPRECATED_VALUE,
- gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE,
- gitlab: Gitlab::UsageData::DEPRECATED_VALUE,
- github: Gitlab::UsageData::DEPRECATED_VALUE,
- bitbucket: Gitlab::UsageData::DEPRECATED_VALUE,
- bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE,
- gitea: Gitlab::UsageData::DEPRECATED_VALUE,
- git: Gitlab::UsageData::DEPRECATED_VALUE,
- manifest: Gitlab::UsageData::DEPRECATED_VALUE
- },
- issues_imported: {
- jira: Gitlab::UsageData::DEPRECATED_VALUE,
- fogbugz: Gitlab::UsageData::DEPRECATED_VALUE,
- phabricator: Gitlab::UsageData::DEPRECATED_VALUE,
- csv: Gitlab::UsageData::DEPRECATED_VALUE
- },
- groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
+ }
}
)
expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
{
bulk_imports: {
- gitlab_v1: 1,
- gitlab: Gitlab::UsageData::DEPRECATED_VALUE
+ gitlab_v1: 1
},
project_imports: {
bitbucket: 1,
@@ -350,25 +328,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
group_imports: {
group_import: 1,
gitlab_migration: 1
- },
- projects_imported: {
- total: Gitlab::UsageData::DEPRECATED_VALUE,
- gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE,
- gitlab: Gitlab::UsageData::DEPRECATED_VALUE,
- github: Gitlab::UsageData::DEPRECATED_VALUE,
- bitbucket: Gitlab::UsageData::DEPRECATED_VALUE,
- bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE,
- gitea: Gitlab::UsageData::DEPRECATED_VALUE,
- git: Gitlab::UsageData::DEPRECATED_VALUE,
- manifest: Gitlab::UsageData::DEPRECATED_VALUE
- },
- issues_imported: {
- jira: Gitlab::UsageData::DEPRECATED_VALUE,
- fogbugz: Gitlab::UsageData::DEPRECATED_VALUE,
- phabricator: Gitlab::UsageData::DEPRECATED_VALUE,
- csv: Gitlab::UsageData::DEPRECATED_VALUE
- },
- groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
+ }
}
)
end
@@ -920,6 +880,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:database][:adapter]).to eq(ApplicationRecord.database.adapter_name)
expect(subject[:database][:version]).to eq(ApplicationRecord.database.version)
expect(subject[:database][:pg_system_id]).to eq(ApplicationRecord.database.system_id)
+ expect(subject[:database][:flavor]).to eq('Cloud SQL for PostgreSQL')
expect(subject[:mail][:smtp_server]).to eq(ActionMailer::Base.smtp_settings[:address])
expect(subject[:gitaly][:version]).to be_present
expect(subject[:gitaly][:servers]).to be >= 1
@@ -964,10 +925,25 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
context 'when retrieve component setting meets exception' do
- it 'returns -1 for component enable status' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(should_raise_for_dev)
allow(Settings).to receive(:[]).with(component).and_raise(StandardError)
+ end
+
+ context 'with should_raise_for_dev? false' do
+ let(:should_raise_for_dev) { false }
+
+ it 'returns -1 for component enable status' do
+ expect(subject).to eq({ enabled: -1 })
+ end
+ end
+
+ context 'with should_raise_for_dev? true' do
+ let(:should_raise_for_dev) { true }
- expect(subject).to eq({ enabled: -1 })
+ it 'raises an error' do
+ expect { subject.value }.to raise_error(StandardError)
+ end
end
end
end
@@ -1328,6 +1304,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
+ let(:ignored_metrics) { ["i_package_composer_deploy_token_weekly"] }
+
it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters)
@@ -1337,6 +1315,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category)
metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" }
+ metrics -= ignored_metrics
if ::Gitlab::UsageDataCounters::HLLRedisCounter::CATEGORIES_FOR_TOTALS.include?(category)
metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly")
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index 325ace6fbbf..b44c6565538 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -5,11 +5,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Utils::UsageData do
include Database::DatabaseHelpers
- shared_examples 'failing hardening method' do
+ shared_examples 'failing hardening method' do |raised_exception|
+ let(:exception) { raised_exception || ActiveRecord::StatementInvalid }
+
before do
allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(should_raise_for_dev)
stub_const("Gitlab::Utils::UsageData::FALLBACK", fallback)
- allow(failing_class).to receive(failing_method).and_raise(ActiveRecord::StatementInvalid)
+ allow(failing_class).to receive(failing_method).and_raise(exception) unless failing_class.nil?
end
context 'with should_raise_for_dev? false' do
@@ -24,7 +26,7 @@ RSpec.describe Gitlab::Utils::UsageData do
let(:should_raise_for_dev) { true }
it 'raises an error' do
- expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
+ expect { subject }.to raise_error(exception)
end
end
end
@@ -366,8 +368,13 @@ RSpec.describe Gitlab::Utils::UsageData do
expect(described_class.add).to eq(0)
end
- it 'returns the fallback value when adding fails' do
- expect(described_class.add(nil, 3)).to eq(-1)
+ context 'when adding fails' do
+ subject { described_class.add(nil, 3) }
+
+ let(:fallback) { -1 }
+ let(:failing_class) { nil }
+
+ it_behaves_like 'failing hardening method', StandardError
end
it 'returns the fallback value one of the arguments is negative' do
@@ -376,8 +383,13 @@ RSpec.describe Gitlab::Utils::UsageData do
end
describe '#alt_usage_data' do
- it 'returns the fallback when it gets an error' do
- expect(described_class.alt_usage_data { raise StandardError } ).to eq(-1)
+ context 'when method fails' do
+ subject { described_class.alt_usage_data { raise StandardError } }
+
+ let(:fallback) { -1 }
+ let(:failing_class) { nil }
+
+ it_behaves_like 'failing hardening method', StandardError
end
it 'returns the evaluated block when give' do
@@ -391,14 +403,22 @@ RSpec.describe Gitlab::Utils::UsageData do
describe '#redis_usage_data' do
context 'with block given' do
- it 'returns the fallback when it gets an error' do
- expect(described_class.redis_usage_data { raise ::Redis::CommandError } ).to eq(-1)
+ context 'when method fails' do
+ subject { described_class.redis_usage_data { raise ::Redis::CommandError } }
+
+ let(:fallback) { -1 }
+ let(:failing_class) { nil }
+
+ it_behaves_like 'failing hardening method', ::Redis::CommandError
end
- it 'returns the fallback when Redis HLL raises any error' do
- stub_const("Gitlab::Utils::UsageData::FALLBACK", 15)
+ context 'when Redis HLL raises any error' do
+ subject { described_class.redis_usage_data { raise Gitlab::UsageDataCounters::HLLRedisCounter::CategoryMismatch } }
+
+ let(:fallback) { 15 }
+ let(:failing_class) { nil }
- expect(described_class.redis_usage_data { raise Gitlab::UsageDataCounters::HLLRedisCounter::CategoryMismatch } ).to eq(15)
+ it_behaves_like 'failing hardening method', Gitlab::UsageDataCounters::HLLRedisCounter::CategoryMismatch
end
it 'returns the evaluated block when given' do
@@ -407,9 +427,14 @@ RSpec.describe Gitlab::Utils::UsageData do
end
context 'with counter given' do
- it 'returns the falback values for all counter keys when it gets an error' do
- allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_raise(::Redis::CommandError)
- expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql(::Gitlab::UsageDataCounters::WikiPageCounter.fallback_totals)
+ context 'when gets an error' do
+ subject { described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter) }
+
+ let(:fallback) { ::Gitlab::UsageDataCounters::WikiPageCounter.fallback_totals }
+ let(:failing_class) { ::Gitlab::UsageDataCounters::WikiPageCounter }
+ let(:failing_method) { :totals }
+
+ it_behaves_like 'failing hardening method', ::Redis::CommandError
end
it 'returns the totals when couter is given' do
diff --git a/spec/lib/gitlab/web_hooks/recursion_detection_spec.rb b/spec/lib/gitlab/web_hooks/recursion_detection_spec.rb
new file mode 100644
index 00000000000..45170864967
--- /dev/null
+++ b/spec/lib/gitlab/web_hooks/recursion_detection_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::WebHooks::RecursionDetection, :clean_gitlab_redis_shared_state, :request_store do
+ let_it_be(:web_hook) { create(:project_hook) }
+
+ let!(:uuid_class) { described_class::UUID }
+
+ describe '.set_from_headers' do
+ let(:old_uuid) { SecureRandom.uuid }
+ let(:rack_headers) { Rack::MockRequest.env_for("/").merge(headers) }
+
+ subject(:set_from_headers) { described_class.set_from_headers(rack_headers) }
+
+ # Note, having a previous `request_uuid` value set before `.set_from_headers` is
+ # called is contrived and should not normally happen. However, testing with this scenario
+ # allows us to assert the ideal outcome if it ever were to happen.
+ before do
+ uuid_class.instance.request_uuid = old_uuid
+ end
+
+ context 'when the detection header is present' do
+ let(:new_uuid) { SecureRandom.uuid }
+
+ let(:headers) do
+ { uuid_class::HEADER => new_uuid }
+ end
+
+ it 'sets the request UUID value from the headers' do
+ set_from_headers
+
+ expect(uuid_class.instance.request_uuid).to eq(new_uuid)
+ end
+ end
+
+ context 'when detection header is not present' do
+ let(:headers) { {} }
+
+ it 'does not set the request UUID' do
+ set_from_headers
+
+ expect(uuid_class.instance.request_uuid).to eq(old_uuid)
+ end
+ end
+ end
+
+ describe '.set_request_uuid' do
+ it 'sets the request UUID value' do
+ new_uuid = SecureRandom.uuid
+
+ described_class.set_request_uuid(new_uuid)
+
+ expect(uuid_class.instance.request_uuid).to eq(new_uuid)
+ end
+ end
+
+ describe '.register!' do
+ let_it_be(:second_web_hook) { create(:project_hook) }
+ let_it_be(:third_web_hook) { create(:project_hook) }
+
+ def cache_key(hook)
+ described_class.send(:cache_key_for_hook, hook)
+ end
+
+ it 'stores IDs in the same cache when a request UUID is set, until the request UUID changes', :aggregate_failures do
+ # Register web_hook and second_web_hook against the same request UUID.
+ uuid_class.instance.request_uuid = SecureRandom.uuid
+ described_class.register!(web_hook)
+ described_class.register!(second_web_hook)
+ first_cache_key = cache_key(web_hook)
+ second_cache_key = cache_key(second_web_hook)
+
+ # Register third_web_hook against a new request UUID.
+ uuid_class.instance.request_uuid = SecureRandom.uuid
+ described_class.register!(third_web_hook)
+ third_cache_key = cache_key(third_web_hook)
+
+ expect(first_cache_key).to eq(second_cache_key)
+ expect(second_cache_key).not_to eq(third_cache_key)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ members = redis.smembers(first_cache_key).map(&:to_i)
+ expect(members).to contain_exactly(web_hook.id, second_web_hook.id)
+
+ members = redis.smembers(third_cache_key).map(&:to_i)
+ expect(members).to contain_exactly(third_web_hook.id)
+ end
+ end
+
+ it 'stores IDs in unique caches when no request UUID is present', :aggregate_failures do
+ described_class.register!(web_hook)
+ described_class.register!(second_web_hook)
+ described_class.register!(third_web_hook)
+
+ first_cache_key = cache_key(web_hook)
+ second_cache_key = cache_key(second_web_hook)
+ third_cache_key = cache_key(third_web_hook)
+
+ expect([first_cache_key, second_cache_key, third_cache_key].compact.length).to eq(3)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ members = redis.smembers(first_cache_key).map(&:to_i)
+ expect(members).to contain_exactly(web_hook.id)
+
+ members = redis.smembers(second_cache_key).map(&:to_i)
+ expect(members).to contain_exactly(second_web_hook.id)
+
+ members = redis.smembers(third_cache_key).map(&:to_i)
+ expect(members).to contain_exactly(third_web_hook.id)
+ end
+ end
+
+ it 'touches the storage ttl each time it is called', :aggregate_failures do
+ freeze_time do
+ described_class.register!(web_hook)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.ttl(cache_key(web_hook))).to eq(described_class::TOUCH_CACHE_TTL.to_i)
+ end
+ end
+
+ travel_to(1.minute.from_now) do
+ described_class.register!(second_web_hook)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.ttl(cache_key(web_hook))).to eq(described_class::TOUCH_CACHE_TTL.to_i)
+ end
+ end
+ end
+ end
+
+ describe 'block?' do
+ let_it_be(:registered_web_hooks) { create_list(:project_hook, 2) }
+
+ subject(:block?) { described_class.block?(web_hook) }
+
+ before do
+ # Register some previous webhooks.
+ uuid_class.instance.request_uuid = SecureRandom.uuid
+
+ registered_web_hooks.each do |web_hook|
+ described_class.register!(web_hook)
+ end
+ end
+
+ it 'returns false if webhook should not be blocked' do
+ is_expected.to eq(false)
+ end
+
+ context 'when the webhook has previously fired' do
+ before do
+ described_class.register!(web_hook)
+ end
+
+ it 'returns true' do
+ is_expected.to eq(true)
+ end
+
+ context 'when the request UUID changes again' do
+ before do
+ uuid_class.instance.request_uuid = SecureRandom.uuid
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+
+ context 'when the count limit has been reached' do
+ let_it_be(:registered_web_hooks) { create_list(:project_hook, 2) }
+
+ before do
+ registered_web_hooks.each do |web_hook|
+ described_class.register!(web_hook)
+ end
+
+ stub_const("#{described_class.name}::COUNT_LIMIT", registered_web_hooks.size)
+ end
+
+ it 'returns true' do
+ is_expected.to eq(true)
+ end
+
+ context 'when the request UUID changes again' do
+ before do
+ uuid_class.instance.request_uuid = SecureRandom.uuid
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+ end
+
+ describe '.header' do
+ subject(:header) { described_class.header(web_hook) }
+
+ it 'returns a header with the UUID value' do
+ uuid = SecureRandom.uuid
+ allow(uuid_class.instance).to receive(:uuid_for_hook).and_return(uuid)
+
+ is_expected.to eq({ uuid_class::HEADER => uuid })
+ end
+ end
+
+ describe '.to_log' do
+ subject(:to_log) { described_class.to_log(web_hook) }
+
+ it 'returns the UUID value and all registered webhook IDs in a Hash' do
+ uuid = SecureRandom.uuid
+ allow(uuid_class.instance).to receive(:uuid_for_hook).and_return(uuid)
+ registered_web_hooks = create_list(:project_hook, 2)
+ registered_web_hooks.each { described_class.register!(_1) }
+
+ is_expected.to eq({ uuid: uuid, ids: registered_web_hooks.map(&:id) })
+ end
+ end
+end
diff --git a/spec/lib/gitlab_edition_spec.rb b/spec/lib/gitlab_edition_spec.rb
new file mode 100644
index 00000000000..2f1316819ec
--- /dev/null
+++ b/spec/lib/gitlab_edition_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabEdition do
+ before do
+ # Make sure the ENV is clean
+ stub_env('FOSS_ONLY', nil)
+ stub_env('EE_ONLY', nil)
+
+ described_class.instance_variable_set(:@is_ee, nil)
+ described_class.instance_variable_set(:@is_jh, nil)
+ end
+
+ after do
+ described_class.instance_variable_set(:@is_ee, nil)
+ described_class.instance_variable_set(:@is_jh, nil)
+ end
+
+ describe '.root' do
+ it 'returns the root path of the app' do
+ expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
+ end
+ end
+
+ describe 'extensions' do
+ context 'when .jh? is true' do
+ before do
+ allow(described_class).to receive(:jh?).and_return(true)
+ end
+
+ it 'returns %w[ee jh]' do
+ expect(described_class.extensions).to match_array(%w[ee jh])
+ end
+ end
+
+ context 'when .ee? is true' do
+ before do
+ allow(described_class).to receive(:jh?).and_return(false)
+ allow(described_class).to receive(:ee?).and_return(true)
+ end
+
+ it 'returns %w[ee]' do
+ expect(described_class.extensions).to match_array(%w[ee])
+ end
+ end
+
+ context 'when neither .jh? and .ee? are true' do
+ before do
+ allow(described_class).to receive(:jh?).and_return(false)
+ allow(described_class).to receive(:ee?).and_return(false)
+ end
+
+ it 'returns the exyensions according to the current edition' do
+ expect(described_class.extensions).to be_empty
+ end
+ end
+ end
+
+ describe '.ee? and .jh?' do
+ def stub_path(*paths, **arguments)
+ root = Pathname.new('dummy')
+ pathname = double(:path, **arguments)
+
+ allow(described_class)
+ .to receive(:root)
+ .and_return(root)
+
+ allow(root).to receive(:join)
+
+ paths.each do |path|
+ allow(root)
+ .to receive(:join)
+ .with(path)
+ .and_return(pathname)
+ end
+ end
+
+ describe '.ee?' do
+ context 'for EE' do
+ before do
+ stub_path('ee/app/models/license.rb', exist?: true)
+ end
+
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
+
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
+ end
+
+ context 'when using FOSS_ONLY=0' do
+ before do
+ stub_env('FOSS_ONLY', '0')
+ end
+
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
+
+ context 'when using default FOSS_ONLY' do
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
+ end
+
+ context 'for CE' do
+ before do
+ stub_path('ee/app/models/license.rb', exist?: false)
+ end
+
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
+ end
+ end
+
+ describe '.jh?' do
+ context 'for JH' do
+ before do
+ stub_path(
+ 'ee/app/models/license.rb',
+ 'jh',
+ exist?: true)
+ end
+
+ context 'when using default FOSS_ONLY and EE_ONLY' do
+ it 'returns to be JH' do
+ expect(described_class).to be_jh
+ end
+ end
+
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
+
+ it 'returns not to be JH' do
+ expect(described_class).not_to be_jh
+ end
+ end
+
+ context 'when using EE_ONLY=1' do
+ before do
+ stub_env('EE_ONLY', '1')
+ end
+
+ it 'returns not to be JH' do
+ expect(described_class).not_to be_jh
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 869eaf26772..49ba4debe31 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -3,9 +3,19 @@
require 'spec_helper'
RSpec.describe Gitlab do
- describe '.root' do
- it 'returns the root path of the app' do
- expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
+ %w[root extensions ee? jh?].each do |method_name|
+ it "delegates #{method_name} to GitlabEdition" do
+ expect(GitlabEdition).to receive(method_name)
+
+ described_class.public_send(method_name)
+ end
+ end
+
+ %w[ee jh].each do |method_name|
+ it "delegates #{method_name} to GitlabEdition" do
+ expect(GitlabEdition).to receive(method_name)
+
+ described_class.public_send(method_name) {}
end
end
@@ -248,121 +258,6 @@ RSpec.describe Gitlab do
end
end
- describe 'ee? and jh?' do
- before do
- # Make sure the ENV is clean
- stub_env('FOSS_ONLY', nil)
- stub_env('EE_ONLY', nil)
-
- described_class.instance_variable_set(:@is_ee, nil)
- described_class.instance_variable_set(:@is_jh, nil)
- end
-
- after do
- described_class.instance_variable_set(:@is_ee, nil)
- described_class.instance_variable_set(:@is_jh, nil)
- end
-
- def stub_path(*paths, **arguments)
- root = Pathname.new('dummy')
- pathname = double(:path, **arguments)
-
- allow(described_class)
- .to receive(:root)
- .and_return(root)
-
- allow(root).to receive(:join)
-
- paths.each do |path|
- allow(root)
- .to receive(:join)
- .with(path)
- .and_return(pathname)
- end
- end
-
- describe '.ee?' do
- context 'for EE' do
- before do
- stub_path('ee/app/models/license.rb', exist?: true)
- end
-
- context 'when using FOSS_ONLY=1' do
- before do
- stub_env('FOSS_ONLY', '1')
- end
-
- it 'returns not to be EE' do
- expect(described_class).not_to be_ee
- end
- end
-
- context 'when using FOSS_ONLY=0' do
- before do
- stub_env('FOSS_ONLY', '0')
- end
-
- it 'returns to be EE' do
- expect(described_class).to be_ee
- end
- end
-
- context 'when using default FOSS_ONLY' do
- it 'returns to be EE' do
- expect(described_class).to be_ee
- end
- end
- end
-
- context 'for CE' do
- before do
- stub_path('ee/app/models/license.rb', exist?: false)
- end
-
- it 'returns not to be EE' do
- expect(described_class).not_to be_ee
- end
- end
- end
-
- describe '.jh?' do
- context 'for JH' do
- before do
- stub_path(
- 'ee/app/models/license.rb',
- 'jh',
- exist?: true)
- end
-
- context 'when using default FOSS_ONLY and EE_ONLY' do
- it 'returns to be JH' do
- expect(described_class).to be_jh
- end
- end
-
- context 'when using FOSS_ONLY=1' do
- before do
- stub_env('FOSS_ONLY', '1')
- end
-
- it 'returns not to be JH' do
- expect(described_class).not_to be_jh
- end
- end
-
- context 'when using EE_ONLY=1' do
- before do
- stub_env('EE_ONLY', '1')
- end
-
- it 'returns not to be JH' do
- expect(described_class).not_to be_jh
- end
- end
- end
- end
- end
-
describe '.http_proxy_env?' do
it 'returns true when lower case https' do
stub_env('https_proxy', 'https://my.proxy')
diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
index 314c4cdc602..252da8ea699 100644
--- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
@@ -56,6 +56,12 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu do
it_behaves_like 'access rights checks'
end
+ describe 'Access Tokens' do
+ let(:item_id) { :access_tokens }
+
+ it_behaves_like 'access rights checks'
+ end
+
describe 'Repository menu' do
let(:item_id) { :repository }
diff --git a/spec/lib/sidebars/projects/panel_spec.rb b/spec/lib/sidebars/projects/panel_spec.rb
index 2e79ced7039..7e69a2dfe52 100644
--- a/spec/lib/sidebars/projects/panel_spec.rb
+++ b/spec/lib/sidebars/projects/panel_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Panel do
- let(:project) { build(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) }
subject { described_class.new(context) }
diff --git a/spec/lib/version_check_spec.rb b/spec/lib/version_check_spec.rb
index d7a772a3f7e..736a8f9595e 100644
--- a/spec/lib/version_check_spec.rb
+++ b/spec/lib/version_check_spec.rb
@@ -3,12 +3,6 @@
require 'spec_helper'
RSpec.describe VersionCheck do
- describe '.image_url' do
- it 'returns the correct URL' do
- expect(described_class.image_url).to match(%r{\A#{Regexp.escape(described_class.host)}/check\.svg\?gitlab_info=\w+})
- end
- end
-
describe '.url' do
it 'returns the correct URL' do
expect(described_class.url).to match(%r{\A#{Regexp.escape(described_class.host)}/check\.json\?gitlab_info=\w+})
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 365ca892bb1..af77989dbbc 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Emails::Profile do
describe 'for users that signed up, the email' do
let(:example_site_path) { root_path }
- let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
+ let(:new_user) { create(:user, email: new_user_address, password: Gitlab::Password.test_default) }
subject { Notify.new_user_email(new_user.id) }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 44cb18008d2..0fbdc09a206 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -213,7 +213,7 @@ RSpec.describe Notify do
subject { described_class.issue_due_email(recipient.id, issue.id) }
before do
- issue.update(due_date: Date.tomorrow)
+ issue.update!(due_date: Date.tomorrow)
end
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -1229,7 +1229,7 @@ RSpec.describe Notify do
end
context 'when a comment on an existing discussion' do
- let(:first_note) { create(model) }
+ let(:first_note) { create(model) } # rubocop:disable Rails/SaveBang
let(:note) { create(model, author: note_author, noteable: nil, in_reply_to: first_note) }
it 'contains an introduction' do
@@ -1505,7 +1505,7 @@ RSpec.describe Notify do
context 'member is not created by a user' do
before do
- group_member.update(created_by: nil)
+ group_member.update!(created_by: nil)
end
it_behaves_like 'no email is sent'
@@ -1513,7 +1513,7 @@ RSpec.describe Notify do
context 'member is a known user' do
before do
- group_member.update(user: create(:user))
+ group_member.update!(user: create(:user))
end
it_behaves_like 'no email is sent'
@@ -1737,7 +1737,7 @@ RSpec.describe Notify do
stub_config_setting(email_subject_suffix: 'A Nice Suffix')
perform_enqueued_jobs do
user.email = "new-email@mail.com"
- user.save
+ user.save!
end
end
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index 4e3c6900875..fc18df9b5cd 100644
--- a/spec/metrics_server/metrics_server_spec.rb
+++ b/spec/metrics_server/metrics_server_spec.rb
@@ -8,18 +8,32 @@ require_relative '../support/helpers/next_instance_of'
RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
include NextInstanceOf
+ let(:prometheus_config) { ::Prometheus::Client.configuration }
+ let(:metrics_dir) { Dir.mktmpdir }
+
+ # Prometheus::Client is a singleton, i.e. shared global state, so
+ # we need to reset it after testing.
+ let!(:old_multiprocess_files_dir) { prometheus_config.multiprocess_files_dir }
+
before do
# We do not want this to have knock-on effects on the test process.
allow(Gitlab::ProcessManagement).to receive(:modify_signals)
end
+ after do
+ Gitlab::Metrics.reset_registry!
+ prometheus_config.multiprocess_files_dir = old_multiprocess_files_dir
+
+ FileUtils.rm_rf(metrics_dir, secure: true)
+ end
+
describe '.spawn' do
context 'when in parent process' do
it 'forks into a new process and detaches it' do
expect(Process).to receive(:fork).and_return(99)
expect(Process).to receive(:detach).with(99)
- described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics')
+ described_class.spawn('sidekiq', metrics_dir: metrics_dir)
end
end
@@ -35,13 +49,13 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
expect(server).to receive(:start)
end
- described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics')
+ described_class.spawn('sidekiq', metrics_dir: metrics_dir)
end
it 'resets signal handlers from parent process' do
expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
- described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics', trapped_signals: %i[A B])
+ described_class.spawn('sidekiq', metrics_dir: metrics_dir, trapped_signals: %i[A B])
end
end
end
@@ -49,29 +63,27 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
describe '#start' do
let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
let(:exporter_double) { double('fake_exporter', start: true) }
- let(:prometheus_config) { ::Prometheus::Client.configuration }
- let(:metrics_dir) { Dir.mktmpdir }
let(:settings) { { "fake_exporter" => { "enabled" => true } } }
- let!(:old_metrics_dir) { prometheus_config.multiprocess_files_dir }
+ let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) }
subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
before do
stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
- expect(exporter_class).to receive(:instance).with(settings['fake_exporter'], synchronous: true).and_return(exporter_double)
+ expect(exporter_class).to receive(:instance).with(
+ settings['fake_exporter'], gc_requests: true, synchronous: true
+ ).and_return(exporter_double)
expect(Settings).to receive(:monitoring).and_return(settings)
- end
- after do
- Gitlab::Metrics.reset_registry!
- FileUtils.rm_rf(metrics_dir, secure: true)
- prometheus_config.multiprocess_files_dir = old_metrics_dir
+ allow(Gitlab::Metrics::Samplers::RubySampler).to receive(:initialize_instance).and_return(ruby_sampler_double)
+ allow(ruby_sampler_double).to receive(:start)
end
it 'configures ::Prometheus::Client' do
metrics_server.start
expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
+ expect(::Prometheus::Client.configuration.pid_provider.call).to eq 'fake_exporter'
end
it 'ensures that metrics directory exists in correct mode (0700)' do
@@ -105,5 +117,11 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
metrics_server.start
end
+
+ it 'starts a RubySampler instance' do
+ expect(ruby_sampler_double).to receive(:start)
+
+ subject.start
+ end
end
end
diff --git a/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb b/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
deleted file mode 100644
index b8dc4d7c8ae..00000000000
--- a/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveDuplicateServices2 do
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:services) { table(:services) }
-
- describe '#up' do
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- namespaces.create!(id: 1, name: 'group', path: 'group')
-
- projects.create!(id: 1, namespace_id: 1) # duplicate services
- projects.create!(id: 2, namespace_id: 1) # normal services
- projects.create!(id: 3, namespace_id: 1) # no services
- projects.create!(id: 4, namespace_id: 1) # duplicate services
- projects.create!(id: 5, namespace_id: 1) # duplicate services
-
- services.create!(id: 1, project_id: 1, type: 'JiraService')
- services.create!(id: 2, project_id: 1, type: 'JiraService')
- services.create!(id: 3, project_id: 2, type: 'JiraService')
- services.create!(id: 4, project_id: 4, type: 'AsanaService')
- services.create!(id: 5, project_id: 4, type: 'AsanaService')
- services.create!(id: 6, project_id: 4, type: 'JiraService')
- services.create!(id: 7, project_id: 4, type: 'JiraService')
- services.create!(id: 8, project_id: 4, type: 'SlackService')
- services.create!(id: 9, project_id: 4, type: 'SlackService')
- services.create!(id: 10, project_id: 5, type: 'JiraService')
- services.create!(id: 11, project_id: 5, type: 'JiraService')
-
- # Services without a project_id should be ignored
- services.create!(id: 12, type: 'JiraService')
- services.create!(id: 13, type: 'JiraService')
- end
-
- it 'schedules background jobs for all projects with duplicate services' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 4)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 5)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
deleted file mode 100644
index e07b5a48909..00000000000
--- a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AlterVsaIssueFirstMentionedInCommitValue, schema: 20210114033715 do
- let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
- let(:value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
- let(:namespaces) { table(:namespaces) }
-
- let(:namespace) { namespaces.create!(id: 1, name: 'group', path: 'group') }
- let(:value_stream) { value_streams.create!(name: 'test', group_id: namespace.id) }
-
- let!(:stage_1) { group_stages.create!(group_value_stream_id: value_stream.id, group_id: namespace.id, name: 'stage 1', start_event_identifier: described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_EE, end_event_identifier: 1) }
- let!(:stage_2) { group_stages.create!(group_value_stream_id: value_stream.id, group_id: namespace.id, name: 'stage 2', start_event_identifier: 2, end_event_identifier: described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_EE) }
- let!(:stage_3) { group_stages.create!(group_value_stream_id: value_stream.id, group_id: namespace.id, name: 'stage 3', start_event_identifier: described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_FOSS, end_event_identifier: 3) }
-
- describe '#up' do
- it 'changes the EE specific identifier values to the FOSS version' do
- migrate!
-
- expect(stage_1.reload.start_event_identifier).to eq(described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_FOSS)
- expect(stage_2.reload.end_event_identifier).to eq(described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_FOSS)
- end
-
- it 'does not change irrelevant records' do
- expect { migrate! }.not_to change { stage_3.reload }
- end
- end
-end
diff --git a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
deleted file mode 100644
index 97438062458..00000000000
--- a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveBadDependencyProxyManifests, schema: 20210128140157 do
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
- let_it_be(:group) { namespaces.create!(type: 'Group', name: 'test', path: 'test') }
-
- let_it_be(:dependency_proxy_manifest_with_content_type) do
- dependency_proxy_manifests.create!(group_id: group.id, file: 'foo', file_name: 'foo', digest: 'asdf1234', content_type: 'content-type' )
- end
-
- let_it_be(:dependency_proxy_manifest_without_content_type) do
- dependency_proxy_manifests.create!(group_id: group.id, file: 'bar', file_name: 'bar', digest: 'fdsa6789')
- end
-
- it 'removes the dependency_proxy_manifests with a content_type', :aggregate_failures do
- expect(dependency_proxy_manifest_with_content_type).to be_present
- expect(dependency_proxy_manifest_without_content_type).to be_present
-
- expect { migrate! }.to change { dependency_proxy_manifests.count }.from(2).to(1)
-
- expect(dependency_proxy_manifests.where.not(content_type: nil)).to be_empty
- expect(dependency_proxy_manifest_without_content_type.reload).to be_present
- end
-end
diff --git a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
deleted file mode 100644
index 4a31d36e2bc..00000000000
--- a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillUpdatedAtAfterRepositoryStorageMove, :sidekiq do
- let_it_be(:projects) { table(:projects) }
- let_it_be(:project_repository_storage_moves) { table(:project_repository_storage_moves) }
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
-
- describe '#up' do
- it 'schedules background jobs for all distinct projects in batches' do
- stub_const("#{described_class}::BATCH_SIZE", 3)
-
- project_1 = projects.create!(id: 1, namespace_id: namespace.id)
- project_2 = projects.create!(id: 2, namespace_id: namespace.id)
- project_3 = projects.create!(id: 3, namespace_id: namespace.id)
- project_4 = projects.create!(id: 4, namespace_id: namespace.id)
- project_5 = projects.create!(id: 5, namespace_id: namespace.id)
- project_6 = projects.create!(id: 6, namespace_id: namespace.id)
- project_7 = projects.create!(id: 7, namespace_id: namespace.id)
- projects.create!(id: 8, namespace_id: namespace.id)
-
- project_repository_storage_moves.create!(id: 1, project_id: project_1.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 2, project_id: project_1.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 3, project_id: project_2.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 4, project_id: project_3.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 5, project_id: project_3.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 6, project_id: project_4.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 7, project_id: project_4.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 8, project_id: project_5.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 9, project_id: project_6.id, source_storage_name: 'default', destination_storage_name: 'default')
- project_repository_storage_moves.create!(id: 10, project_id: project_7.id, source_storage_name: 'default', destination_storage_name: 'default')
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(3)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2.minutes, 1, 2, 3)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(4.minutes, 4, 5, 6)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(6.minutes, 7)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb b/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb
deleted file mode 100644
index 039ce53cac4..00000000000
--- a/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddEnvironmentScopeToGroupVariables do
- let(:migration) { described_class.new }
- let(:ci_group_variables) { table(:ci_group_variables) }
- let(:group) { table(:namespaces).create!(name: 'group', path: 'group') }
-
- def create_variable!(group, key:, environment_scope: '*')
- table(:ci_group_variables).create!(
- group_id: group.id,
- key: key,
- environment_scope: environment_scope
- )
- end
-
- describe '#down' do
- context 'group has variables with duplicate keys' do
- it 'deletes all but the first record' do
- migration.up
-
- remaining_variable = create_variable!(group, key: 'key')
- create_variable!(group, key: 'key', environment_scope: 'staging')
- create_variable!(group, key: 'key', environment_scope: 'production')
-
- migration.down
-
- expect(ci_group_variables.pluck(:id)).to eq [remaining_variable.id]
- end
- end
-
- context 'group does not have variables with duplicate keys' do
- it 'does not delete any records' do
- migration.up
-
- create_variable!(group, key: 'key')
- create_variable!(group, key: 'staging')
- create_variable!(group, key: 'production')
-
- expect { migration.down }.not_to change { ci_group_variables.count }
- end
- end
- end
-end
diff --git a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
deleted file mode 100644
index 1b57bf0431f..00000000000
--- a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DedupIssueMetrics, :migration, schema: 20210205104425 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:issues) { table(:issues) }
- let(:metrics) { table(:issue_metrics) }
- let(:issue_params) { { title: 'title', project_id: project.id } }
-
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:issue_1) { issues.create!(issue_params) }
- let!(:issue_2) { issues.create!(issue_params) }
- let!(:issue_3) { issues.create!(issue_params) }
-
- let!(:duplicated_metrics_1) { metrics.create!(issue_id: issue_1.id, first_mentioned_in_commit_at: 1.day.ago, first_added_to_board_at: 5.days.ago, updated_at: 2.months.ago) }
- let!(:duplicated_metrics_2) { metrics.create!(issue_id: issue_1.id, first_mentioned_in_commit_at: Time.now, first_associated_with_milestone_at: Time.now, updated_at: 1.month.ago) }
-
- let!(:duplicated_metrics_3) { metrics.create!(issue_id: issue_3.id, first_mentioned_in_commit_at: 1.day.ago, updated_at: 2.months.ago) }
- let!(:duplicated_metrics_4) { metrics.create!(issue_id: issue_3.id, first_added_to_board_at: 1.day.ago, updated_at: 1.month.ago) }
-
- let!(:non_duplicated_metrics) { metrics.create!(issue_id: issue_2.id, first_added_to_board_at: 2.days.ago) }
-
- it 'deduplicates issue_metrics table' do
- expect { migrate! }.to change { metrics.count }.from(5).to(3)
- end
-
- it 'merges `duplicated_metrics_1` with `duplicated_metrics_2`' do
- migrate!
-
- expect(metrics.where(id: duplicated_metrics_1.id)).not_to exist
-
- merged_metrics = metrics.find_by(id: duplicated_metrics_2.id)
-
- expect(merged_metrics).to be_present
- expect(merged_metrics.first_mentioned_in_commit_at).to be_like_time(duplicated_metrics_2.first_mentioned_in_commit_at)
- expect(merged_metrics.first_added_to_board_at).to be_like_time(duplicated_metrics_1.first_added_to_board_at)
- end
-
- it 'merges `duplicated_metrics_3` with `duplicated_metrics_4`' do
- migrate!
-
- expect(metrics.where(id: duplicated_metrics_3.id)).not_to exist
-
- merged_metrics = metrics.find_by(id: duplicated_metrics_4.id)
-
- expect(merged_metrics).to be_present
- expect(merged_metrics.first_mentioned_in_commit_at).to be_like_time(duplicated_metrics_3.first_mentioned_in_commit_at)
- expect(merged_metrics.first_added_to_board_at).to be_like_time(duplicated_metrics_4.first_added_to_board_at)
- end
-
- it 'does not change non duplicated records' do
- expect { migrate! }.not_to change { non_duplicated_metrics.reload.attributes }
- end
-
- it 'does nothing when there are no metrics' do
- metrics.delete_all
-
- migrate!
-
- expect(metrics.count).to eq(0)
- end
-end
diff --git a/spec/migrations/20210918202855_reschedule_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/migrations/20210918202855_reschedule_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
deleted file mode 100644
index 5a2531bb63f..00000000000
--- a/spec/migrations/20210918202855_reschedule_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210918202855_reschedule_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid.rb')
-
-RSpec.describe ReschedulePendingJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
-
- context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are pending' do
- before do
- background_migration_jobs.create!(
- class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
- arguments: [1, 2, 3],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
- )
- background_migration_jobs.create!(
- class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
- arguments: [4, 5, 6],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
- )
- end
-
- it 'queues pending jobs' do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.length).to eq(1)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['RecalculateVulnerabilitiesOccurrencesUuid', [1, 2, 3]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
- end
- end
-end
diff --git a/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
new file mode 100644
index 00000000000..491aad1b30b
--- /dev/null
+++ b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+def create_background_migration_jobs(ids, status, created_at)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: created_at
+ )
+end
+
+RSpec.describe RemoveJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration do
+ let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+
+ context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
+ before do
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4))
+
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2))
+ create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4))
+ end
+
+ it 'removes all jobs' do
+ expect(background_migration_jobs.count).to eq(5)
+
+ migrate!
+
+ expect(background_migration_jobs.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences3_spec.rb b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
index 77f298b5ecb..71ffcafaae1 100644
--- a/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences3_spec.rb
+++ b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
+RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences4 do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
@@ -13,6 +13,7 @@ RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
let(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
let(:vulnerabilities) { table(:vulnerabilities) }
let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let(:vulnerability_identifier) do
vulnerability_identifiers.create!(
@@ -32,6 +33,17 @@ RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
name: 'Identifier for UUIDv4')
end
+ let!(:uuidv4_finding) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_uuidv4.id,
+ project_id: project.id,
+ scanner_id: different_scanner.id,
+ primary_identifier_id: different_vulnerability_identifier.id,
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('fa18f432f1d56675f4098d318739c3cd5b14eb3e'),
+ uuid: 'b3cc2518-5446-4dea-871c-89d5e999c1ac'
+ )
+ end
+
let(:vulnerability_for_uuidv4) do
create_vulnerability!(
project_id: project.id,
@@ -39,6 +51,17 @@ RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
)
end
+ let!(:uuidv5_finding) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_uuidv5.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('838574be0210968bf6b9f569df9c2576242cbf0a'),
+ uuid: '77211ed6-7dff-5f6b-8c9a-da89ad0a9b60'
+ )
+ end
+
let(:vulnerability_for_uuidv5) do
create_vulnerability!(
project_id: project.id,
@@ -46,25 +69,22 @@ RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
)
end
- let!(:finding1) do
- create_finding!(
- vulnerability_id: vulnerability_for_uuidv4.id,
+ let(:vulnerability_for_finding_with_signature) do
+ create_vulnerability!(
project_id: project.id,
- scanner_id: different_scanner.id,
- primary_identifier_id: different_vulnerability_identifier.id,
- location_fingerprint: 'fa18f432f1d56675f4098d318739c3cd5b14eb3e',
- uuid: 'b3cc2518-5446-4dea-871c-89d5e999c1ac'
+ author_id: user.id
)
end
- let!(:finding2) do
+ let!(:finding_with_signature) do
create_finding!(
- vulnerability_id: vulnerability_for_uuidv5.id,
+ vulnerability_id: vulnerability_for_finding_with_signature.id,
project_id: project.id,
scanner_id: scanner.id,
primary_identifier_id: vulnerability_identifier.id,
- location_fingerprint: '838574be0210968bf6b9f569df9c2576242cbf0a',
- uuid: '77211ed6-7dff-5f6b-8c9a-da89ad0a9b60'
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('123609eafffffa2207a9ca2425ba4337h34fga1b'),
+ uuid: '252aa474-d689-5d2b-ab42-7bbb5a100c02'
)
end
@@ -79,9 +99,10 @@ RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
it 'schedules background migrations', :aggregate_failures do
migrate!
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, finding1.id, finding1.id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, finding2.id, finding2.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(3)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, uuidv4_finding.id, uuidv4_finding.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, uuidv5_finding.id, uuidv5_finding.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(6.minutes, finding_with_signature.id, finding_with_signature.id)
end
private
@@ -98,14 +119,14 @@ RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences3 do
end
def create_finding!(
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, location_fingerprint:, uuid:)
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, location_fingerprint:, uuid:, report_type: 0)
vulnerabilities_findings.create!(
vulnerability_id: vulnerability_id,
project_id: project_id,
name: 'test',
severity: 7,
confidence: 7,
- report_type: 0,
+ report_type: report_type,
project_fingerprint: '123qweasdzxc',
scanner_id: scanner_id,
primary_identifier_id: primary_identifier_id,
diff --git a/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
new file mode 100644
index 00000000000..289cf9a93ed
--- /dev/null
+++ b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe EncryptStaticObjectToken, :migration do
+ let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+ let_it_be(:users) { table(:users) }
+
+ let!(:user_without_tokens) { create_user!(name: 'notoken') }
+ let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') }
+ let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') }
+ let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') }
+ let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migrations' do
+ migrate!
+
+ expect(background_migration_jobs.count).to eq(2)
+ expect(background_migration_jobs.first.arguments).to match_array([user_with_plaintext_token_1.id, user_with_plaintext_token_1.id])
+ expect(background_migration_jobs.second.arguments).to match_array([user_with_plaintext_token_2.id, user_with_plaintext_token_2.id])
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, user_with_plaintext_token_1.id, user_with_plaintext_token_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, user_with_plaintext_token_2.id, user_with_plaintext_token_2.id)
+ end
+
+ private
+
+ def create_user!(name:, token: nil, encrypted_token: nil)
+ email = "#{name}@example.com"
+
+ table(:users).create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ static_object_token: token,
+ static_object_token_encrypted: encrypted_token
+ )
+ end
+end
diff --git a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
new file mode 100644
index 00000000000..a17fee6bab2
--- /dev/null
+++ b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillIncidentIssueEscalationStatuses do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+ end
+
+ it 'schedules jobs for incident issues' do
+ issue_1 = issues.create!(project_id: project.id) # non-incident issue
+ incident_1 = issues.create!(project_id: project.id, issue_type: 1)
+ incident_2 = issues.create!(project_id: project.id, issue_type: 1)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 2.minutes, issue_1.id, issue_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 4.minutes, incident_1.id, incident_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 6.minutes, incident_2.id, incident_2.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(3)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
new file mode 100644
index 00000000000..c5058f30d82
--- /dev/null
+++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+def create_background_migration_jobs(ids, status, created_at)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: created_at
+ )
+end
+
+RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration do
+ let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+
+ context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
+ before do
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4))
+
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2))
+ create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4))
+ end
+
+ describe 'gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'marks all jobs as succeeded' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(5)
+ end
+ end
+
+ describe 'self managed' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not change job status' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/add_has_external_issue_tracker_trigger_spec.rb b/spec/migrations/add_has_external_issue_tracker_trigger_spec.rb
deleted file mode 100644
index 72983c7cfbf..00000000000
--- a/spec/migrations/add_has_external_issue_tracker_trigger_spec.rb
+++ /dev/null
@@ -1,164 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe AddHasExternalIssueTrackerTrigger do
- let(:migration) { described_class.new }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
-
- before do
- @namespace = namespaces.create!(name: 'foo', path: 'foo')
- @project = projects.create!(namespace_id: @namespace.id)
- end
-
- describe '#up' do
- before do
- migrate!
- end
-
- describe 'INSERT trigger' do
- it 'sets `has_external_issue_tracker` to true when active `issue_tracker` is inserted' do
- expect do
- services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- end.to change { @project.reload.has_external_issue_tracker }.to(true)
- end
-
- it 'does not set `has_external_issue_tracker` to true when service is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
-
- expect do
- services.create!(category: 'issue_tracker', active: true, project_id: different_project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not set `has_external_issue_tracker` to true when inactive `issue_tracker` is inserted' do
- expect do
- services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not set `has_external_issue_tracker` to true when a non-`issue tracker` active service is inserted' do
- expect do
- services.create!(category: 'my_type', active: true, project_id: @project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
-
- describe 'UPDATE trigger' do
- it 'sets `has_external_issue_tracker` to true when `issue_tracker` is made active' do
- service = services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
-
- expect do
- service.update!(active: true)
- end.to change { @project.reload.has_external_issue_tracker }.to(true)
- end
-
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is made inactive' do
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- service.update!(active: false)
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is made inactive, and an inactive `issue_tracker` exists' do
- services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- service.update!(active: false)
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'does not change `has_external_issue_tracker` when `issue_tracker` is made inactive, if an active `issue_tracker` exists' do
- services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- service.update!(active: false)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not change `has_external_issue_tracker` when service is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- service = services.create!(category: 'issue_tracker', active: false, project_id: different_project.id)
-
- expect do
- service.update!(active: true)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
-
- describe 'DELETE trigger' do
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is deleted' do
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- service.delete
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is deleted, if an inactive `issue_tracker` still exists' do
- services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- service.delete
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'does not change `has_external_issue_tracker` when `issue_tracker` is deleted, if an active `issue_tracker` still exists' do
- services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- service.delete
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not change `has_external_issue_tracker` when service is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- service = services.create!(category: 'issue_tracker', active: true, project_id: different_project.id)
-
- expect do
- service.delete
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
- end
-
- describe '#down' do
- before do
- migration.up
- migration.down
- end
-
- it 'drops the INSERT trigger' do
- expect do
- services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'drops the UPDATE trigger' do
- service = services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- @project.update!(has_external_issue_tracker: false)
-
- expect do
- service.update!(active: true)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'drops the DELETE trigger' do
- service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- @project.update!(has_external_issue_tracker: true)
-
- expect do
- service.delete
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
-end
diff --git a/spec/migrations/add_has_external_wiki_trigger_spec.rb b/spec/migrations/add_has_external_wiki_trigger_spec.rb
deleted file mode 100644
index 10c6888c87e..00000000000
--- a/spec/migrations/add_has_external_wiki_trigger_spec.rb
+++ /dev/null
@@ -1,128 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe AddHasExternalWikiTrigger do
- let(:migration) { described_class.new }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
-
- before do
- @namespace = namespaces.create!(name: 'foo', path: 'foo')
- @project = projects.create!(namespace_id: @namespace.id)
- end
-
- describe '#up' do
- before do
- migrate!
- end
-
- describe 'INSERT trigger' do
- it 'sets `has_external_wiki` to true when active `ExternalWikiService` is inserted' do
- expect do
- services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
- end.to change { @project.reload.has_external_wiki }.to(true)
- end
-
- it 'does not set `has_external_wiki` to true when service is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
-
- expect do
- services.create!(type: 'ExternalWikiService', active: true, project_id: different_project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
-
- it 'does not set `has_external_wiki` to true when inactive `ExternalWikiService` is inserted' do
- expect do
- services.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
-
- it 'does not set `has_external_wiki` to true when active other service is inserted' do
- expect do
- services.create!(type: 'MyService', active: true, project_id: @project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
-
- describe 'UPDATE trigger' do
- it 'sets `has_external_wiki` to true when `ExternalWikiService` is made active' do
- service = services.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
-
- expect do
- service.update!(active: true)
- end.to change { @project.reload.has_external_wiki }.to(true)
- end
-
- it 'sets `has_external_wiki` to false when `ExternalWikiService` is made inactive' do
- service = services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
-
- expect do
- service.update!(active: false)
- end.to change { @project.reload.has_external_wiki }.to(false)
- end
-
- it 'does not change `has_external_wiki` when service is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- service = services.create!(type: 'ExternalWikiService', active: false, project_id: different_project.id)
-
- expect do
- service.update!(active: true)
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
-
- describe 'DELETE trigger' do
- it 'sets `has_external_wiki` to false when `ExternalWikiService` is deleted' do
- service = services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
-
- expect do
- service.delete
- end.to change { @project.reload.has_external_wiki }.to(false)
- end
-
- it 'does not change `has_external_wiki` when service is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- service = services.create!(type: 'ExternalWikiService', active: true, project_id: different_project.id)
-
- expect do
- service.delete
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
- end
-
- describe '#down' do
- before do
- migration.up
- migration.down
- end
-
- it 'drops the INSERT trigger' do
- expect do
- services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
-
- it 'drops the UPDATE trigger' do
- service = services.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
- @project.update!(has_external_wiki: false)
-
- expect do
- service.update!(active: true)
- end.not_to change { @project.reload.has_external_wiki }
- end
-
- it 'drops the DELETE trigger' do
- service = services.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
- @project.update!(has_external_wiki: true)
-
- expect do
- service.delete
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
-end
diff --git a/spec/migrations/add_new_post_eoa_plans_spec.rb b/spec/migrations/add_new_post_eoa_plans_spec.rb
deleted file mode 100644
index 02360d5a12d..00000000000
--- a/spec/migrations/add_new_post_eoa_plans_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe AddNewPostEoaPlans do
- let(:plans) { table(:plans) }
-
- subject(:migration) { described_class.new }
-
- describe '#up' do
- it 'creates the two new records' do
- expect { migration.up }.to change { plans.count }.by(2)
-
- new_plans = plans.last(2)
- expect(new_plans.map(&:name)).to contain_exactly('premium', 'ultimate')
- end
- end
-
- describe '#down' do
- it 'removes these two new records' do
- plans.create!(name: 'premium', title: 'Premium (Formerly Silver)')
- plans.create!(name: 'ultimate', title: 'Ultimate (Formerly Gold)')
-
- expect { migration.down }.to change { plans.count }.by(-2)
-
- expect(plans.find_by(name: 'premium')).to be_nil
- expect(plans.find_by(name: 'ultimate')).to be_nil
- end
- end
-end
diff --git a/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
new file mode 100644
index 00000000000..abff7c6aba1
--- /dev/null
+++ b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
+ let(:migration) { described_class.new }
+ let(:users) { table(:users) }
+ let(:emails) { table(:emails) }
+
+ let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 3.days.ago, projects_limit: 100) }
+ let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_4) { users.create!(name: 'unconfirmed-user', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) }
+
+ let!(:email_1) { emails.create!(email: 'confirmed-1@example.com', user_id: user_1.id, confirmed_at: 1.day.ago) }
+ let!(:email_2) { emails.create!(email: 'other_2@example.com', user_id: user_2.id, confirmed_at: 1.day.ago) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'consume any pending background migration job' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
+ expect(coordinator).to receive(:steal).with('AddPrimaryEmailToEmailsIfUserConfirmed').twice
+ end
+
+ migration.up
+ end
+
+ it 'adds the primary email to emails for leftover confirmed users that do not have their primary email in the emails table', :aggregate_failures do
+ original_email_1_confirmed_at = email_1.reload.confirmed_at
+
+ expect { migration.up }.to change { emails.count }.by(2)
+
+ expect(emails.find_by(user_id: user_2.id, email: 'confirmed-2@example.com').confirmed_at).to eq(user_2.reload.confirmed_at)
+ expect(emails.find_by(user_id: user_3.id, email: 'confirmed-3@example.com').confirmed_at).to eq(user_3.reload.confirmed_at)
+ expect(email_1.reload.confirmed_at).to eq(original_email_1_confirmed_at)
+
+ expect(emails.exists?(user_id: user_4.id)).to be(false)
+ end
+
+ it 'continues in case of errors with one email' do
+ allow(Email).to receive(:create) { raise 'boom!' }
+
+ expect { migration.up }.not_to raise_error
+ end
+end
diff --git a/spec/migrations/cleanup_projects_with_bad_has_external_issue_tracker_data_spec.rb b/spec/migrations/cleanup_projects_with_bad_has_external_issue_tracker_data_spec.rb
deleted file mode 100644
index 8aedd1f9607..00000000000
--- a/spec/migrations/cleanup_projects_with_bad_has_external_issue_tracker_data_spec.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe CleanupProjectsWithBadHasExternalIssueTrackerData, :migration do
- let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
-
- def create_projects!(num)
- Array.new(num) do
- projects.create!(namespace_id: namespace.id)
- end
- end
-
- def create_active_external_issue_tracker_integrations!(*projects)
- projects.each do |project|
- services.create!(category: 'issue_tracker', project_id: project.id, active: true)
- end
- end
-
- def create_disabled_external_issue_tracker_integrations!(*projects)
- projects.each do |project|
- services.create!(category: 'issue_tracker', project_id: project.id, active: false)
- end
- end
-
- def create_active_other_integrations!(*projects)
- projects.each do |project|
- services.create!(category: 'not_an_issue_tracker', project_id: project.id, active: true)
- end
- end
-
- it 'sets `projects.has_external_issue_tracker` correctly' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
-
- project_with_an_external_issue_tracker_1,
- project_with_an_external_issue_tracker_2,
- project_with_only_a_disabled_external_issue_tracker_1,
- project_with_only_a_disabled_external_issue_tracker_2,
- project_without_any_external_issue_trackers_1,
- project_without_any_external_issue_trackers_2 = create_projects!(6)
-
- create_active_external_issue_tracker_integrations!(
- project_with_an_external_issue_tracker_1,
- project_with_an_external_issue_tracker_2
- )
-
- create_disabled_external_issue_tracker_integrations!(
- project_with_an_external_issue_tracker_1,
- project_with_an_external_issue_tracker_2,
- project_with_only_a_disabled_external_issue_tracker_1,
- project_with_only_a_disabled_external_issue_tracker_2
- )
-
- create_active_other_integrations!(
- project_with_an_external_issue_tracker_1,
- project_with_an_external_issue_tracker_2,
- project_without_any_external_issue_trackers_1,
- project_without_any_external_issue_trackers_2
- )
-
- # PG triggers on the services table added in
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51852 will have set
- # the `has_external_issue_tracker` columns to correct data when the services
- # records were created above.
- #
- # We set the `has_external_issue_tracker` columns for projects to incorrect
- # data manually below to emulate projects in a state before the PG
- # triggers were added.
- project_with_an_external_issue_tracker_2.update!(has_external_issue_tracker: false)
- project_with_only_a_disabled_external_issue_tracker_2.update!(has_external_issue_tracker: true)
- project_without_any_external_issue_trackers_2.update!(has_external_issue_tracker: true)
-
- migrate!
-
- expected_true = [
- project_with_an_external_issue_tracker_1,
- project_with_an_external_issue_tracker_2
- ].each(&:reload).map(&:has_external_issue_tracker)
-
- expected_not_true = [
- project_without_any_external_issue_trackers_1,
- project_without_any_external_issue_trackers_2,
- project_with_only_a_disabled_external_issue_tracker_1,
- project_with_only_a_disabled_external_issue_tracker_2
- ].each(&:reload).map(&:has_external_issue_tracker)
-
- expect(expected_true).to all(eq(true))
- expect(expected_not_true).to all(be_falsey)
- end
-end
diff --git a/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
deleted file mode 100644
index ee1f718849f..00000000000
--- a/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe CleanupProjectsWithBadHasExternalWikiData, :migration do
- let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
-
- def create_projects!(num)
- Array.new(num) do
- projects.create!(namespace_id: namespace.id)
- end
- end
-
- def create_active_external_wiki_integrations!(*projects)
- projects.each do |project|
- services.create!(type: 'ExternalWikiService', project_id: project.id, active: true)
- end
- end
-
- def create_disabled_external_wiki_integrations!(*projects)
- projects.each do |project|
- services.create!(type: 'ExternalWikiService', project_id: project.id, active: false)
- end
- end
-
- def create_active_other_integrations!(*projects)
- projects.each do |project|
- services.create!(type: 'NotAnExternalWikiService', project_id: project.id, active: true)
- end
- end
-
- it 'sets `projects.has_external_wiki` correctly' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
-
- project_with_external_wiki_1,
- project_with_external_wiki_2,
- project_with_disabled_external_wiki_1,
- project_with_disabled_external_wiki_2,
- project_without_external_wiki_1,
- project_without_external_wiki_2 = create_projects!(6)
-
- create_active_external_wiki_integrations!(
- project_with_external_wiki_1,
- project_with_external_wiki_2
- )
-
- create_disabled_external_wiki_integrations!(
- project_with_disabled_external_wiki_1,
- project_with_disabled_external_wiki_2
- )
-
- create_active_other_integrations!(
- project_without_external_wiki_1,
- project_without_external_wiki_2
- )
-
- # PG triggers on the services table added in a previous migration
- # will have set the `has_external_wiki` columns to correct data when
- # the services records were created above.
- #
- # We set the `has_external_wiki` columns for projects to incorrect
- # data manually below to emulate projects in a state before the PG
- # triggers were added.
- project_with_external_wiki_2.update!(has_external_wiki: false)
- project_with_disabled_external_wiki_2.update!(has_external_wiki: true)
- project_without_external_wiki_2.update!(has_external_wiki: true)
-
- migrate!
-
- expected_true = [
- project_with_external_wiki_1,
- project_with_external_wiki_2
- ].each(&:reload).map(&:has_external_wiki)
-
- expected_not_true = [
- project_without_external_wiki_1,
- project_without_external_wiki_2,
- project_with_disabled_external_wiki_1,
- project_with_disabled_external_wiki_2
- ].each(&:reload).map(&:has_external_wiki)
-
- expect(expected_true).to all(eq(true))
- expect(expected_not_true).to all(be_falsey)
- end
-end
diff --git a/spec/migrations/drop_alerts_service_data_spec.rb b/spec/migrations/drop_alerts_service_data_spec.rb
deleted file mode 100644
index 06382132952..00000000000
--- a/spec/migrations/drop_alerts_service_data_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DropAlertsServiceData do
- let_it_be(:alerts_service_data) { table(:alerts_service_data) }
-
- it 'correctly migrates up and down' do
- reversible_migration do |migration|
- migration.before -> {
- expect(alerts_service_data.create!(service_id: 1)).to be_a alerts_service_data
- }
-
- migration.after -> {
- expect { alerts_service_data.create!(service_id: 1) }
- .to raise_error(ActiveRecord::StatementInvalid, /UndefinedTable/)
- }
- end
- end
-end
diff --git a/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb
deleted file mode 100644
index 0f45cc842ef..00000000000
--- a/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings, :migration do
- let(:namespaces) { table(:namespaces) }
- let(:namespace_settings) { table(:namespace_settings) }
-
- let!(:namespace_wo_settings) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) }
- let!(:namespace_wo_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) }
- let!(:namespace_w_settings_delay_true) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) }
- let!(:namespace_w_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) }
-
- let!(:namespace_settings_delay_true) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_true.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) }
- let!(:namespace_settings_delay_false) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_false.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) }
-
- it 'migrates delayed_project_removal to namespace_settings' do
- disable_migrations_output { migrate! }
-
- expect(namespace_settings.count).to eq(3)
-
- expect(namespace_settings.find_by(namespace_id: namespace_wo_settings.id).delayed_project_removal).to eq(true)
- expect(namespace_settings.find_by(namespace_id: namespace_wo_settings_delay_false.id)).to be_nil
-
- expect(namespace_settings_delay_true.reload.delayed_project_removal).to eq(true)
- expect(namespace_settings_delay_false.reload.delayed_project_removal).to eq(false)
- end
-end
diff --git a/spec/migrations/remove_alerts_service_records_again_spec.rb b/spec/migrations/remove_alerts_service_records_again_spec.rb
deleted file mode 100644
index 94d3e957b6a..00000000000
--- a/spec/migrations/remove_alerts_service_records_again_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveAlertsServiceRecordsAgain do
- let(:services) { table(:services) }
-
- before do
- 5.times { services.create!(type: 'AlertsService') }
- services.create!(type: 'SomeOtherType')
- end
-
- it 'removes services records of type AlertsService and corresponding data', :aggregate_failures do
- expect(services.count).to eq(6)
-
- migrate!
-
- expect(services.count).to eq(1)
- expect(services.first.type).to eq('SomeOtherType')
- expect(services.where(type: 'AlertsService')).to be_empty
- end
-end
diff --git a/spec/migrations/remove_alerts_service_records_spec.rb b/spec/migrations/remove_alerts_service_records_spec.rb
deleted file mode 100644
index 83f440f8e17..00000000000
--- a/spec/migrations/remove_alerts_service_records_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveAlertsServiceRecords do
- let(:services) { table(:services) }
- let(:alerts_service_data) { table(:alerts_service_data) }
-
- before do
- 5.times do
- service = services.create!(type: 'AlertsService')
- alerts_service_data.create!(service_id: service.id)
- end
-
- services.create!(type: 'SomeOtherType')
- end
-
- it 'removes services records of type AlertsService and corresponding data', :aggregate_failures do
- expect(services.count).to eq(6)
- expect(alerts_service_data.count).to eq(5)
-
- migrate!
-
- expect(services.count).to eq(1)
- expect(services.first.type).to eq('SomeOtherType')
- expect(services.where(type: 'AlertsService')).to be_empty
- expect(alerts_service_data.all).to be_empty
- end
-end
diff --git a/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb b/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb
deleted file mode 100644
index c06ce3d5bea..00000000000
--- a/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe RescheduleArtifactExpiryBackfill, :migration do
- let(:migration_class) { Gitlab::BackgroundMigration::BackfillArtifactExpiryDate }
- let(:migration_name) { migration_class.to_s.demodulize }
-
- before do
- table(:namespaces).create!(id: 123, name: 'test_namespace', path: 'test_namespace')
- table(:projects).create!(id: 123, name: 'sample_project', path: 'sample_project', namespace_id: 123)
- end
-
- it 'correctly schedules background migrations' do
- first_artifact = create_artifact(job_id: 0, expire_at: nil, created_at: Date.new(2020, 06, 21))
- second_artifact = create_artifact(job_id: 1, expire_at: nil, created_at: Date.new(2020, 06, 21))
- create_artifact(job_id: 2, expire_at: Date.yesterday, created_at: Date.new(2020, 06, 21))
- create_artifact(job_id: 3, expire_at: nil, created_at: Date.new(2020, 06, 23))
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- expect(migration_name).to be_scheduled_migration_with_multiple_args(first_artifact.id, second_artifact.id)
- end
- end
- end
-
- private
-
- def create_artifact(params)
- table(:ci_builds).create!(id: params[:job_id], project_id: 123)
- table(:ci_job_artifacts).create!(project_id: 123, file_type: 1, **params)
- end
-end
diff --git a/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
index 29e4cf05c2b..52bbd5b4f6e 100644
--- a/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
+++ b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleMigratePagesToZipStorage, :sidekiq_might_not_need_inline, schema: 20201231133921 do
+RSpec.describe ScheduleMigratePagesToZipStorage, :sidekiq_might_not_need_inline, schema: 20210301200959 do
let(:migration_class) { described_class::MIGRATION }
let(:migration_name) { migration_class.to_s.demodulize }
diff --git a/spec/migrations/schedule_populate_finding_uuid_for_vulnerability_feedback_spec.rb b/spec/migrations/schedule_populate_finding_uuid_for_vulnerability_feedback_spec.rb
deleted file mode 100644
index d8bdefd5546..00000000000
--- a/spec/migrations/schedule_populate_finding_uuid_for_vulnerability_feedback_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SchedulePopulateFindingUuidForVulnerabilityFeedback do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:users) { table(:users) }
- let(:vulnerability_feedback) { table(:vulnerability_feedback) }
-
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
- let(:user) { users.create!(username: 'john.doe', projects_limit: 1) }
-
- let(:common_feedback_params) { { feedback_type: 0, category: 0, project_id: project.id, author_id: user.id } }
- let!(:feedback_1) { vulnerability_feedback.create!(**common_feedback_params, project_fingerprint: 'foo') }
- let!(:feedback_2) { vulnerability_feedback.create!(**common_feedback_params, project_fingerprint: 'bar') }
- let!(:feedback_3) { vulnerability_feedback.create!(**common_feedback_params, project_fingerprint: 'zoo', finding_uuid: SecureRandom.uuid) }
-
- around do |example|
- freeze_time { Sidekiq::Testing.fake! { example.run } }
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- it 'schedules the background jobs', :aggregate_failures do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(3)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2.minutes, feedback_1.id, feedback_1.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(4.minutes, feedback_2.id, feedback_2.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(6.minutes, feedback_3.id, feedback_3.id)
- end
-end
diff --git a/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb b/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb
deleted file mode 100644
index e7d1813e428..00000000000
--- a/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb
+++ /dev/null
@@ -1,127 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences2 do
- let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let(:users) { table(:users) }
- let(:user) { create_user! }
- let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
- let(:scanners) { table(:vulnerability_scanners) }
- let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
- let(:vulnerabilities) { table(:vulnerabilities) }
- let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let(:vulnerability_identifier) do
- vulnerability_identifiers.create!(
- project_id: project.id,
- external_type: 'uuid-v5',
- external_id: 'uuid-v5',
- fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
- name: 'Identifier for UUIDv5')
- end
-
- let(:different_vulnerability_identifier) do
- vulnerability_identifiers.create!(
- project_id: project.id,
- external_type: 'uuid-v4',
- external_id: 'uuid-v4',
- fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89',
- name: 'Identifier for UUIDv4')
- end
-
- let(:vulnerability_for_uuidv4) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id
- )
- end
-
- let(:vulnerability_for_uuidv5) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id
- )
- end
-
- let!(:finding1) do
- create_finding!(
- vulnerability_id: vulnerability_for_uuidv4.id,
- project_id: project.id,
- scanner_id: different_scanner.id,
- primary_identifier_id: different_vulnerability_identifier.id,
- location_fingerprint: 'fa18f432f1d56675f4098d318739c3cd5b14eb3e',
- uuid: 'b3cc2518-5446-4dea-871c-89d5e999c1ac'
- )
- end
-
- let!(:finding2) do
- create_finding!(
- vulnerability_id: vulnerability_for_uuidv5.id,
- project_id: project.id,
- scanner_id: scanner.id,
- primary_identifier_id: vulnerability_identifier.id,
- location_fingerprint: '838574be0210968bf6b9f569df9c2576242cbf0a',
- uuid: '77211ed6-7dff-5f6b-8c9a-da89ad0a9b60'
- )
- end
-
- before do
- stub_const("#{described_class}::BATCH_SIZE", 1)
- end
-
- around do |example|
- freeze_time { Sidekiq::Testing.fake! { example.run } }
- end
-
- it 'schedules background migrations', :aggregate_failures do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, finding1.id, finding1.id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, finding2.id, finding2.id)
- end
-
- private
-
- def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
- vulnerabilities.create!(
- project_id: project_id,
- author_id: author_id,
- title: title,
- severity: severity,
- confidence: confidence,
- report_type: report_type
- )
- end
-
- def create_finding!(
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, location_fingerprint:, uuid:)
- vulnerabilities_findings.create!(
- vulnerability_id: vulnerability_id,
- project_id: project_id,
- name: 'test',
- severity: 7,
- confidence: 7,
- report_type: 0,
- project_fingerprint: '123qweasdzxc',
- scanner_id: scanner_id,
- primary_identifier_id: primary_identifier_id,
- location_fingerprint: location_fingerprint,
- metadata_version: 'test',
- raw_metadata: 'test',
- uuid: uuid
- )
- end
-
- def create_user!(name: "Example User", email: "user@example.com", user_type: nil)
- users.create!(
- name: name,
- email: email,
- username: name,
- projects_limit: 0
- )
- end
-end
diff --git a/spec/migrations/update_application_settings_protected_paths_spec.rb b/spec/migrations/update_application_settings_protected_paths_spec.rb
new file mode 100644
index 00000000000..21879995f1b
--- /dev/null
+++ b/spec/migrations/update_application_settings_protected_paths_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateApplicationSettingsProtectedPaths, :aggregate_failures do
+ subject(:migration) { described_class.new }
+
+ let_it_be(:application_settings) { table(:application_settings) }
+ let_it_be(:oauth_paths) { %w[/oauth/authorize /oauth/token] }
+ let_it_be(:custom_paths) { %w[/foo /bar] }
+
+ let(:default_paths) { application_settings.column_defaults.fetch('protected_paths') }
+
+ before do
+ application_settings.create!(protected_paths: custom_paths)
+ application_settings.create!(protected_paths: custom_paths + oauth_paths)
+ application_settings.create!(protected_paths: custom_paths + oauth_paths.take(1))
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ application_settings.reset_column_information
+ end
+
+ it 'removes the OAuth paths from the default value and persisted records' do
+ expect(default_paths).not_to include(*oauth_paths)
+ expect(default_paths).to eq(described_class::NEW_DEFAULT_PROTECTED_PATHS)
+ expect(application_settings.all).to all(have_attributes(protected_paths: custom_paths))
+ end
+ end
+
+ describe '#down' do
+ before do
+ migrate!
+ schema_migrate_down!
+ end
+
+ it 'adds the OAuth paths to the default value and persisted records' do
+ expect(default_paths).to include(*oauth_paths)
+ expect(default_paths).to eq(described_class::OLD_DEFAULT_PROTECTED_PATHS)
+ expect(application_settings.all).to all(have_attributes(protected_paths: custom_paths + oauth_paths))
+ end
+ end
+end
diff --git a/spec/migrations/update_invalid_member_states_spec.rb b/spec/migrations/update_invalid_member_states_spec.rb
new file mode 100644
index 00000000000..802634230a9
--- /dev/null
+++ b/spec/migrations/update_invalid_member_states_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe UpdateInvalidMemberStates do
+ let(:members) { table(:members) }
+ let(:groups) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+
+ before do
+ user = users.create!(first_name: 'Test', last_name: 'User', email: 'test@user.com', projects_limit: 1)
+ group = groups.create!(name: 'gitlab', path: 'gitlab-org')
+ project = projects.create!(namespace_id: group.id)
+
+ members.create!(state: 2, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0)
+ members.create!(state: 2, source_id: project.id, source_type: 'Project', type: 'ProjectMember', user_id: user.id, access_level: 50, notification_level: 0)
+ members.create!(state: 1, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0)
+ members.create!(state: 0, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0)
+ end
+
+ it 'updates matching member record states' do
+ expect { migrate! }
+ .to change { members.where(state: 0).count }.from(1).to(3)
+ .and change { members.where(state: 2).count }.from(2).to(0)
+ .and change { members.where(state: 1).count }.by(0)
+ end
+end
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 35398e29062..40bdfd4bc92 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -211,12 +211,6 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '.open' do
- subject { described_class.open }
-
- it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
- end
-
describe '.not_resolved' do
subject { described_class.not_resolved }
@@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '.open_status?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:status, :is_open_status) do
- :triggered | true
- :acknowledged | true
- :resolved | false
- :ignored | false
- nil | false
- end
-
- with_them do
- it 'returns true when the status is open status' do
- expect(described_class.open_status?(status)).to eq(is_open_status)
- end
- end
- end
-
- describe '#open?' do
- it 'returns true when the status is open status' do
- expect(triggered_alert.open?).to be true
- expect(acknowledged_alert.open?).to be true
- expect(resolved_alert.open?).to be false
- expect(ignored_alert.open?).to be false
- end
- end
-
describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index f0212da3041..9c9a048999c 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -147,22 +147,20 @@ RSpec.describe ApplicationRecord do
end
end
- # rubocop:disable Database/MultipleDatabases
it 'increments a counter when a transaction is created in ActiveRecord' do
expect(described_class.connection.transaction_open?).to be false
expect(::Gitlab::Database::Metrics)
.to receive(:subtransactions_increment)
- .with('ActiveRecord::Base')
+ .with('ApplicationRecord')
.once
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.transaction(requires_new: true) do
- expect(ActiveRecord::Base.connection.transaction_open?).to be true
+ ApplicationRecord.transaction do
+ ApplicationRecord.transaction(requires_new: true) do
+ expect(ApplicationRecord.connection.transaction_open?).to be true
end
end
end
- # rubocop:enable Database/MultipleDatabases
end
describe '.with_fast_read_statement_timeout' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 67314084c4f..0ece212d692 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -77,9 +77,24 @@ RSpec.describe ApplicationSetting do
it { is_expected.to validate_numericality_of(:container_registry_cleanup_tags_service_max_list_size).only_integer.is_greater_than_or_equal_to(0) }
it { is_expected.to validate_numericality_of(:container_registry_expiration_policies_worker_capacity).only_integer.is_greater_than_or_equal_to(0) }
+ it { is_expected.to validate_numericality_of(:container_registry_import_max_tags_count).only_integer.is_greater_than_or_equal_to(0) }
+ it { is_expected.to validate_numericality_of(:container_registry_import_max_retries).only_integer.is_greater_than_or_equal_to(0) }
+ it { is_expected.to validate_numericality_of(:container_registry_import_start_max_retries).only_integer.is_greater_than_or_equal_to(0) }
+ it { is_expected.to validate_numericality_of(:container_registry_import_max_step_duration).only_integer.is_greater_than_or_equal_to(0) }
+ it { is_expected.not_to allow_value(nil).for(:container_registry_import_max_tags_count) }
+ it { is_expected.not_to allow_value(nil).for(:container_registry_import_max_retries) }
+ it { is_expected.not_to allow_value(nil).for(:container_registry_import_start_max_retries) }
+ it { is_expected.not_to allow_value(nil).for(:container_registry_import_max_step_duration) }
+
+ it { is_expected.to validate_presence_of(:container_registry_import_target_plan) }
+ it { is_expected.to validate_presence_of(:container_registry_import_created_before) }
+
it { is_expected.to validate_numericality_of(:dependency_proxy_ttl_group_policy_worker_capacity).only_integer.is_greater_than_or_equal_to(0) }
it { is_expected.not_to allow_value(nil).for(:dependency_proxy_ttl_group_policy_worker_capacity) }
+ it { is_expected.to validate_numericality_of(:packages_cleanup_package_file_worker_capacity).only_integer.is_greater_than_or_equal_to(0) }
+ it { is_expected.not_to allow_value(nil).for(:packages_cleanup_package_file_worker_capacity) }
+
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
it { is_expected.to validate_presence_of(:max_artifacts_size) }
@@ -126,11 +141,13 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") }
- it { is_expected.to allow_value(400).for(:notes_create_limit) }
- it { is_expected.not_to allow_value('two').for(:notes_create_limit) }
- it { is_expected.not_to allow_value(nil).for(:notes_create_limit) }
- it { is_expected.not_to allow_value(5.5).for(:notes_create_limit) }
- it { is_expected.not_to allow_value(-2).for(:notes_create_limit) }
+ %i[notes_create_limit user_email_lookup_limit].each do |setting|
+ it { is_expected.to allow_value(400).for(setting) }
+ it { is_expected.not_to allow_value('two').for(setting) }
+ it { is_expected.not_to allow_value(nil).for(setting) }
+ it { is_expected.not_to allow_value(5.5).for(setting) }
+ it { is_expected.not_to allow_value(-2).for(setting) }
+ end
def many_usernames(num = 100)
Array.new(num) { |i| "username#{i}" }
@@ -489,7 +506,7 @@ RSpec.describe ApplicationSetting do
context 'key restrictions' do
it 'supports all key types' do
- expect(described_class::SUPPORTED_KEY_TYPES).to contain_exactly(:rsa, :dsa, :ecdsa, :ed25519)
+ expect(described_class::SUPPORTED_KEY_TYPES).to eq(Gitlab::SSHPublicKey.supported_types)
end
it 'does not allow all key types to be disabled' do
@@ -1242,7 +1259,7 @@ RSpec.describe ApplicationSetting do
end
end
- describe '#static_objects_external_storage_auth_token=' do
+ describe '#static_objects_external_storage_auth_token=', :aggregate_failures do
subject { setting.static_objects_external_storage_auth_token = token }
let(:token) { 'Test' }
@@ -1266,5 +1283,20 @@ RSpec.describe ApplicationSetting do
expect(setting.static_objects_external_storage_auth_token).to be_nil
end
end
+
+ context 'with plaintext token only' do
+ let(:token) { '' }
+
+ it 'ignores the plaintext token' do
+ subject
+
+ ApplicationSetting.update_all(static_objects_external_storage_auth_token: 'Test')
+
+ setting.reload
+ expect(setting[:static_objects_external_storage_auth_token]).to be_nil
+ expect(setting[:static_objects_external_storage_auth_token_encrypted]).to be_nil
+ expect(setting.static_objects_external_storage_auth_token).to be_nil
+ end
+ end
end
end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 02151da583e..61caff647d6 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -91,4 +91,10 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
end
end
end
+
+ describe '#file_relations' do
+ it 'returns project file relations' do
+ expect(subject.file_relations).to contain_exactly('uploads', 'lfs_objects')
+ end
+ end
end
diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb
index e78f602feef..3f53c6c1c0e 100644
--- a/spec/models/ci/build_report_result_spec.rb
+++ b/spec/models/ci/build_report_result_spec.rb
@@ -5,6 +5,11 @@ require 'spec_helper'
RSpec.describe Ci::BuildReportResult do
let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) }
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_build_report_result, project: parent) }
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:build) }
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index b9a12339e61..b8c5af5a911 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -565,6 +565,26 @@ RSpec.describe Ci::Build do
expect(build.reload.runtime_metadata).not_to be_present
end
end
+
+ context 'when a failure reason is provided' do
+ context 'when a failure reason is a symbol' do
+ it 'correctly sets a failure reason' do
+ build.drop!(:script_failure)
+
+ expect(build.failure_reason).to eq 'script_failure'
+ end
+ end
+
+ context 'when a failure reason is an object' do
+ it 'correctly sets a failure reason' do
+ reason = ::Gitlab::Ci::Build::Status::Reason.new(build, :script_failure)
+
+ build.drop!(reason)
+
+ expect(build.failure_reason).to eq 'script_failure'
+ end
+ end
+ end
end
describe '#schedulable?' do
@@ -2002,6 +2022,16 @@ RSpec.describe Ci::Build do
it { is_expected.not_to be_retryable }
end
+
+ context 'when build is waiting for deployment approval' do
+ subject { build_stubbed(:ci_build, :manual, environment: 'production') }
+
+ before do
+ create(:deployment, :blocked, deployable: subject)
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
end
end
@@ -2064,6 +2094,31 @@ RSpec.describe Ci::Build do
end
describe 'build auto retry feature' do
+ context 'with deployment job' do
+ let(:build) do
+ create(:ci_build, :deploy_to_production, :with_deployment,
+ user: user, pipeline: pipeline, project: project)
+ end
+
+ before do
+ project.add_developer(user)
+ allow(build).to receive(:auto_retry_allowed?) { true }
+ end
+
+ it 'creates a deployment when a build is dropped' do
+ expect { build.drop!(:script_failure) }.to change { Deployment.count }.by(1)
+
+ retried_deployment = Deployment.last
+ expect(build.deployment.environment).to eq(retried_deployment.environment)
+ expect(build.deployment.ref).to eq(retried_deployment.ref)
+ expect(build.deployment.sha).to eq(retried_deployment.sha)
+ expect(build.deployment.tag).to eq(retried_deployment.tag)
+ expect(build.deployment.user).to eq(retried_deployment.user)
+ expect(build.deployment).to be_failed
+ expect(retried_deployment).to be_created
+ end
+ end
+
describe '#retries_count' do
subject { create(:ci_build, name: 'test', pipeline: pipeline) }
@@ -2152,6 +2207,28 @@ RSpec.describe Ci::Build do
end
end
+ describe '#auto_retry_expected?' do
+ subject { create(:ci_build, :failed) }
+
+ context 'when build is failed and auto retry is configured' do
+ before do
+ allow(subject)
+ .to receive(:auto_retry_allowed?)
+ .and_return(true)
+ end
+
+ it 'expects auto-retry to happen' do
+ expect(subject.auto_retry_expected?).to be true
+ end
+ end
+
+ context 'when build failed by auto retry is not configured' do
+ it 'does not expect auto-retry to happen' do
+ expect(subject.auto_retry_expected?).to be false
+ end
+ end
+ end
+
describe '#artifacts_file_for_type' do
let(:build) { create(:ci_build, :artifacts) }
let(:file_type) { :archive }
@@ -2443,6 +2520,16 @@ RSpec.describe Ci::Build do
it { is_expected.not_to be_playable }
end
+
+ context 'when build is waiting for deployment approval' do
+ subject { build_stubbed(:ci_build, :manual, environment: 'production') }
+
+ before do
+ create(:deployment, :blocked, deployable: subject)
+ end
+
+ it { is_expected.not_to be_playable }
+ end
end
describe 'project settings' do
@@ -2653,6 +2740,8 @@ RSpec.describe Ci::Build do
{ key: 'CI_DEPENDENCY_PROXY_USER', value: 'gitlab-ci-token', public: true, masked: false },
{ key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: 'my-token', public: false, masked: true },
{ key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true },
+ { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true },
+ { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true },
{ key: 'CI_JOB_NAME', value: 'test', public: true, masked: false },
{ key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false },
{ key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
@@ -2720,6 +2809,7 @@ RSpec.describe Ci::Build do
before do
allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt')
+ allow(Gitlab::Ci::JwtV2).to receive(:for_build).and_return('ci.job.jwtv2')
build.set_token('my-token')
build.yaml_variables = []
end
@@ -2771,6 +2861,8 @@ RSpec.describe Ci::Build do
let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } }
let(:dependency_proxy_var) { { key: 'dependency_proxy', value: 'value', public: true, masked: false } }
let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } }
+ let(:job_jwt_var_v1) { { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true } }
+ let(:job_jwt_var_v2) { { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true } }
let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } }
before do
@@ -2784,7 +2876,7 @@ RSpec.describe Ci::Build do
allow(build).to receive(:dependency_variables) { [job_dependency_var] }
allow(build).to receive(:dependency_proxy_variables) { [dependency_proxy_var] }
- allow(build.project)
+ allow(build.pipeline.project)
.to receive(:predefined_variables) { [project_pre_var] }
project.variables.create!(key: 'secret', value: 'value')
@@ -3084,7 +3176,7 @@ RSpec.describe Ci::Build do
context 'when the branch is protected' do
before do
- allow(build.project).to receive(:protected_for?).with(ref).and_return(true)
+ allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true)
end
it { is_expected.to include(protected_variable) }
@@ -3092,7 +3184,7 @@ RSpec.describe Ci::Build do
context 'when the tag is protected' do
before do
- allow(build.project).to receive(:protected_for?).with(ref).and_return(true)
+ allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true)
end
it { is_expected.to include(protected_variable) }
@@ -3131,7 +3223,7 @@ RSpec.describe Ci::Build do
context 'when the branch is protected' do
before do
- allow(build.project).to receive(:protected_for?).with(ref).and_return(true)
+ allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true)
end
it { is_expected.to include(protected_variable) }
@@ -3139,7 +3231,7 @@ RSpec.describe Ci::Build do
context 'when the tag is protected' do
before do
- allow(build.project).to receive(:protected_for?).with(ref).and_return(true)
+ allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true)
end
it { is_expected.to include(protected_variable) }
@@ -3526,6 +3618,20 @@ RSpec.describe Ci::Build do
build.scoped_variables
end
+
+ context 'when variables builder is used' do
+ it 'returns the same variables' do
+ build.user = create(:user)
+
+ allow(build.pipeline).to receive(:use_variables_builder_definitions?).and_return(false)
+ legacy_variables = build.scoped_variables.to_hash
+
+ allow(build.pipeline).to receive(:use_variables_builder_definitions?).and_return(true)
+ new_variables = build.scoped_variables.to_hash
+
+ expect(new_variables).to eq(legacy_variables)
+ end
+ end
end
describe '#simple_variables_without_dependencies' do
@@ -3538,7 +3644,8 @@ RSpec.describe Ci::Build do
shared_examples "secret CI variables" do
context 'when ref is branch' do
- let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, ref: 'master', tag: false, pipeline: pipeline, project: project) }
context 'when ref is protected' do
before do
@@ -3554,7 +3661,8 @@ RSpec.describe Ci::Build do
end
context 'when ref is tag' do
- let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, pipeline: pipeline, project: project) }
context 'when ref is protected' do
before do
@@ -3652,8 +3760,6 @@ RSpec.describe Ci::Build do
.and_return(project_variables)
end
- it { is_expected.to eq(project_variables) }
-
context 'environment is nil' do
let(:environment) { nil }
@@ -3661,6 +3767,35 @@ RSpec.describe Ci::Build do
end
end
+ describe '#user_variables' do
+ subject { build.user_variables.to_hash }
+
+ context 'with user' do
+ let(:expected_variables) do
+ {
+ 'GITLAB_USER_EMAIL' => user.email,
+ 'GITLAB_USER_ID' => user.id.to_s,
+ 'GITLAB_USER_LOGIN' => user.username,
+ 'GITLAB_USER_NAME' => user.name
+ }
+ end
+
+ before do
+ build.user = user
+ end
+
+ it { is_expected.to eq(expected_variables) }
+ end
+
+ context 'without user' do
+ before do
+ expect(build).to receive(:user).and_return(nil)
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '#any_unmet_prerequisites?' do
let(:build) { create(:ci_build, :created) }
@@ -3762,6 +3897,18 @@ RSpec.describe Ci::Build do
end
end
+ describe 'when the build is waiting for deployment approval' do
+ let(:build) { create(:ci_build, :manual, environment: 'production') }
+
+ before do
+ create(:deployment, :blocked, deployable: build)
+ end
+
+ it 'does not allow the build to be enqueued' do
+ expect { build.enqueue! }.to raise_error(StateMachines::InvalidTransition)
+ end
+ end
+
describe 'state transition: any => [:pending]' do
let(:build) { create(:ci_build, :created) }
@@ -5174,25 +5321,32 @@ RSpec.describe Ci::Build do
.to change { build.reload.failed? }
end
- it 'is executed inside a transaction' do
- expect(build).to receive(:drop!)
- .with(:unknown_failure)
- .and_raise(ActiveRecord::Rollback)
-
- expect(build).to receive(:conditionally_allow_failure!)
- .with(1)
- .and_call_original
-
- expect { drop_with_exit_code }
- .not_to change { build.reload.allow_failure }
- end
-
context 'when exit_code is nil' do
let(:exit_code) {}
it_behaves_like 'drops the build without changing allow_failure'
end
end
+
+ context 'when build is configured to be retried' do
+ let(:options) { { retry: 3 } }
+
+ context 'when there is an MR attached to the pipeline and a failed job todo for that MR' do
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, head_pipeline: pipeline) }
+ let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: user, target: merge_request) }
+
+ before do
+ build.update!(user: user)
+ project.add_developer(user)
+ end
+
+ it 'resolves the todo for the old failed build' do
+ expect do
+ drop_with_exit_code
+ end.to change { todo.reload.state }.from('pending').to('done')
+ end
+ end
+ end
end
describe '#exit_codes_defined?' do
@@ -5377,7 +5531,8 @@ RSpec.describe Ci::Build do
describe '#doom!' do
subject { build.doom! }
- let_it_be(:build) { create(:ci_build, :queued) }
+ let(:traits) { [] }
+ let(:build) { create(:ci_build, *traits, pipeline: pipeline) }
it 'updates status and failure_reason', :aggregate_failures do
subject
@@ -5386,10 +5541,33 @@ RSpec.describe Ci::Build do
expect(build.failure_reason).to eq("data_integrity_failure")
end
- it 'drops associated pending build' do
+ it 'logs a message' do
+ expect(Gitlab::AppLogger)
+ .to receive(:info)
+ .with(a_hash_including(message: 'Build doomed', class: build.class.name, build_id: build.id))
+ .and_call_original
+
subject
+ end
+
+ context 'with queued builds' do
+ let(:traits) { [:queued] }
+
+ it 'drops associated pending build' do
+ subject
- expect(build.reload.queuing_entry).not_to be_present
+ expect(build.reload.queuing_entry).not_to be_present
+ end
+ end
+
+ context 'with running builds' do
+ let(:traits) { [:picked] }
+
+ it 'drops associated runtime metadata' do
+ subject
+
+ expect(build.reload.runtime_metadata).not_to be_present
+ end
end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index b6e128c317c..31c7c7a44bc 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -49,9 +49,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
end
context 'FastDestroyAll' do
- let(:parent) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: parent) }
- let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: parent) }
+ let(:pipeline) { create(:ci_pipeline) }
+ let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline) }
let(:subjects) { build.trace_chunks }
describe 'Forbid #destroy and #destroy_all' do
@@ -84,7 +83,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
expect(external_data_counter).to be > 0
expect(subjects.count).to be > 0
- expect { parent.destroy! }.not_to raise_error
+ expect { pipeline.destroy! }.not_to raise_error
expect(subjects.count).to eq(0)
expect(external_data_counter).to eq(0)
@@ -830,7 +829,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
expect(described_class.count).to eq(3)
- subject
+ expect(subject).to be_truthy
expect(described_class.count).to eq(0)
@@ -852,7 +851,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
context 'when project is destroyed' do
let(:subject) do
- project.destroy!
+ Projects::DestroyService.new(project, project.owner).execute
end
it_behaves_like 'deletes all build_trace_chunk and data in redis'
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index acc87c61036..43ba4c32477 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -164,4 +164,16 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
end
end
end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:model) { create(:ci_daily_build_group_report_result) }
+
+ let!(:parent) { model.group }
+ end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:model) { create(:ci_daily_build_group_report_result) }
+
+ let!(:parent) { model.project }
+ end
end
diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb
index f7f840c6696..b9bf1657e28 100644
--- a/spec/models/ci/freeze_period_spec.rb
+++ b/spec/models/ci/freeze_period_spec.rb
@@ -5,6 +5,11 @@ require 'spec_helper'
RSpec.describe Ci::FreezePeriod, type: :model do
subject { build(:ci_freeze_period) }
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_freeze_period, project: parent) }
+ end
+
let(:invalid_cron) { '0 0 0 * *' }
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index f0eec549da7..4cb3b9eef0c 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -42,4 +42,10 @@ RSpec.describe Ci::GroupVariable do
end
end
end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:model) { create(:ci_group_variable) }
+
+ let!(:parent) { model.group }
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 38061e0975f..2e8c41b410a 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -143,6 +143,17 @@ RSpec.describe Ci::JobArtifact do
end
end
+ describe '.erasable_file_types' do
+ subject { described_class.erasable_file_types }
+
+ it 'returns a list of erasable file types' do
+ all_types = described_class.file_types.keys
+ erasable_types = all_types - described_class::NON_ERASABLE_FILE_TYPES
+
+ expect(subject).to contain_exactly(*erasable_types)
+ end
+ end
+
describe '.erasable' do
subject { described_class.erasable }
@@ -534,20 +545,8 @@ RSpec.describe Ci::JobArtifact do
context 'when the artifact is a trace' do
let(:file_type) { :trace }
- context 'when ci_store_trace_outside_transaction is enabled' do
- it 'returns true' do
- expect(artifact.store_after_commit?).to be_truthy
- end
- end
-
- context 'when ci_store_trace_outside_transaction is disabled' do
- before do
- stub_feature_flags(ci_store_trace_outside_transaction: false)
- end
-
- it 'returns false' do
- expect(artifact.store_after_commit?).to be_falsey
- end
+ it 'returns true' do
+ expect(artifact.store_after_commit?).to be_truthy
end
end
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
index dd6a75dfd89..8d7bb44bd16 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -9,6 +9,11 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
let_it_be(:project) { create(:project) }
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:user) }
+ let!(:model) { create(:ci_job_token_project_scope_link, added_by: parent) }
+ end
+
describe 'unique index' do
let!(:link) { create(:ci_job_token_project_scope_link) }
diff --git a/spec/models/ci/namespace_mirror_spec.rb b/spec/models/ci/namespace_mirror_spec.rb
index b4c71f51377..a9d916115fc 100644
--- a/spec/models/ci/namespace_mirror_spec.rb
+++ b/spec/models/ci/namespace_mirror_spec.rb
@@ -8,50 +8,91 @@ RSpec.describe Ci::NamespaceMirror do
let!(:group3) { create(:group, parent: group2) }
let!(:group4) { create(:group, parent: group3) }
- describe '.sync!' do
- let!(:event) { namespace.sync_events.create! }
+ before do
+ # refreshing ci mirrors according to the parent tree above
+ Namespaces::SyncEvent.find_each { |event| Ci::NamespaceMirror.sync!(event) }
+
+ # checking initial situation. we need to reload to reflect the changes of event sync
+ expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
+ expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id])
+ expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
+ expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id, group4.id])
+ end
+
+ context 'scopes' do
+ describe '.contains_namespace' do
+ let_it_be(:another_group) { create(:group) }
+
+ subject(:result) { described_class.contains_namespace(group2.id) }
+
+ it 'returns groups having group2.id in traversal_ids' do
+ expect(result.pluck(:namespace_id)).to contain_exactly(group2.id, group3.id, group4.id)
+ end
+ end
+
+ describe '.contains_any_of_namespaces' do
+ let!(:other_group1) { create(:group) }
+ let!(:other_group2) { create(:group, parent: other_group1) }
+ let!(:other_group3) { create(:group, parent: other_group2) }
+
+ subject(:result) { described_class.contains_any_of_namespaces([group2.id, other_group2.id]) }
+
+ it 'returns groups having group2.id in traversal_ids' do
+ expect(result.pluck(:namespace_id)).to contain_exactly(
+ group2.id, group3.id, group4.id, other_group2.id, other_group3.id
+ )
+ end
+ end
+
+ describe '.by_namespace_id' do
+ subject(:result) { described_class.by_namespace_id(group2.id) }
+
+ it 'returns namesapce mirrors of namespace id' do
+ expect(result).to contain_exactly(group2.ci_namespace_mirror)
+ end
+ end
+ end
- subject(:sync) { described_class.sync!(event.reload) }
+ describe '.sync!' do
+ subject(:sync) { described_class.sync!(Namespaces::SyncEvent.last) }
- context 'when namespace hierarchy does not exist in the first place' do
+ context 'when namespace mirror does not exist in the first place' do
let(:namespace) { group3 }
- it 'creates the hierarchy' do
- expect { sync }.to change { described_class.count }.from(0).to(1)
+ before do
+ namespace.ci_namespace_mirror.destroy!
+ namespace.sync_events.create!
+ end
+
+ it 'creates the mirror' do
+ expect { sync }.to change { described_class.count }.from(3).to(4)
- expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
+ expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
end
end
- context 'when namespace hierarchy does already exist' do
+ context 'when namespace mirror does already exist' do
let(:namespace) { group3 }
before do
- described_class.create!(namespace: namespace, traversal_ids: [namespace.id])
+ namespace.sync_events.create!
end
- it 'updates the hierarchy' do
+ it 'updates the mirror' do
expect { sync }.not_to change { described_class.count }
- expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
+ expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
end
end
- # I did not extract this context to a `shared_context` because the behavior will change
- # after implementing the TODO in `Ci::NamespaceMirror.sync!`
- context 'changing the middle namespace' do
+ shared_context 'changing the middle namespace' do
let(:namespace) { group2 }
before do
- described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id])
- described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id])
- described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
- described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id])
-
- group2.update!(parent: nil)
+ group2.update!(parent: nil) # creates a sync event
end
- it 'updates hierarchies for the base but wait for events for the children' do
+ it 'updates traversal_ids for the base and descendants' do
expect { sync }.not_to change { described_class.count }
expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
@@ -61,6 +102,8 @@ RSpec.describe Ci::NamespaceMirror do
end
end
+ it_behaves_like 'changing the middle namespace'
+
context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do
before do
stub_feature_flags(sync_traversal_ids: false,
@@ -68,27 +111,7 @@ RSpec.describe Ci::NamespaceMirror do
use_traversal_ids_for_ancestors: false)
end
- context 'changing the middle namespace' do
- let(:namespace) { group2 }
-
- before do
- described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id])
- described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id])
- described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
- described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id])
-
- group2.update!(parent: nil)
- end
-
- it 'updates hierarchies for the base and descendants' do
- expect { sync }.not_to change { described_class.count }
-
- expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
- expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id])
- expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id])
- expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id, group4.id])
- end
- end
+ it_behaves_like 'changing the middle namespace'
end
end
end
diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb
index abf0fb443bb..5692444339f 100644
--- a/spec/models/ci/pending_build_spec.rb
+++ b/spec/models/ci/pending_build_spec.rb
@@ -223,4 +223,14 @@ RSpec.describe Ci::PendingBuild do
end
end
end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:namespace) }
+ let!(:model) { create(:ci_pending_build, namespace: parent) }
+ end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_pending_build, project: parent) }
+ end
end
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index f65483d2290..801505f0231 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -215,4 +215,11 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
end
+
+ context 'loose foreign key on ci_pipeline_artifacts.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_pipeline_artifact, project: parent) }
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index fee74f8f674..0f1cb721e95 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -23,6 +23,11 @@ RSpec.describe Ci::PipelineSchedule do
subject { build(:ci_pipeline_schedule, project: project) }
end
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:user) }
+ let!(:model) { create(:ci_pipeline_schedule, owner: parent) }
+ end
+
describe 'validations' do
it 'does not allow invalid cron patterns' do
pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *')
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index fd9970699d7..90f56c1e0a4 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to have_many(:statuses_order_id_desc) }
it { is_expected.to have_many(:bridges) }
it { is_expected.to have_many(:job_artifacts).through(:builds) }
+ it { is_expected.to have_many(:build_trace_chunks).through(:builds) }
it { is_expected.to have_many(:auto_canceled_pipelines) }
it { is_expected.to have_many(:auto_canceled_jobs) }
it { is_expected.to have_many(:sourced_pipelines) }
@@ -1516,30 +1517,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
describe 'pipeline caching' do
- context 'when expire_job_and_pipeline_cache_synchronously is enabled' do
- before do
- stub_feature_flags(expire_job_and_pipeline_cache_synchronously: true)
- end
-
- it 'executes Ci::ExpirePipelineCacheService' do
- expect_next_instance_of(Ci::ExpirePipelineCacheService) do |service|
- expect(service).to receive(:execute).with(pipeline)
- end
-
- pipeline.cancel
+ it 'executes Ci::ExpirePipelineCacheService' do
+ expect_next_instance_of(Ci::ExpirePipelineCacheService) do |service|
+ expect(service).to receive(:execute).with(pipeline)
end
- end
-
- context 'when expire_job_and_pipeline_cache_synchronously is disabled' do
- before do
- stub_feature_flags(expire_job_and_pipeline_cache_synchronously: false)
- end
-
- it 'performs ExpirePipelinesCacheWorker' do
- expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
- pipeline.cancel
- end
+ pipeline.cancel
end
end
@@ -4677,4 +4660,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let!(:model) { create(:ci_pipeline, user: create(:user)) }
let!(:parent) { model.user }
end
+
+ describe 'tags count' do
+ let_it_be_with_refind(:pipeline) do
+ create(:ci_empty_pipeline, project: project)
+ end
+
+ it { expect(pipeline.tags_count).to eq(0) }
+ it { expect(pipeline.distinct_tags_count).to eq(0) }
+
+ context 'with builds' do
+ before do
+ create(:ci_build, pipeline: pipeline, tag_list: %w[a b])
+ create(:ci_build, pipeline: pipeline, tag_list: %w[b c])
+ end
+
+ it { expect(pipeline.tags_count).to eq(4) }
+ it { expect(pipeline.distinct_tags_count).to eq(3) }
+ end
+ end
end
diff --git a/spec/models/ci/project_mirror_spec.rb b/spec/models/ci/project_mirror_spec.rb
index 199285b036c..5ef520b4230 100644
--- a/spec/models/ci/project_mirror_spec.rb
+++ b/spec/models/ci/project_mirror_spec.rb
@@ -8,12 +8,36 @@ RSpec.describe Ci::ProjectMirror do
let!(:project) { create(:project, namespace: group2) }
+ context 'scopes' do
+ let_it_be(:another_project) { create(:project, namespace: group1) }
+
+ describe '.by_project_id' do
+ subject(:result) { described_class.by_project_id(project.id) }
+
+ it 'returns project mirrors of project' do
+ expect(result.pluck(:project_id)).to contain_exactly(project.id)
+ end
+ end
+
+ describe '.by_namespace_id' do
+ subject(:result) { described_class.by_namespace_id(group2.id) }
+
+ it 'returns project mirrors of namespace id' do
+ expect(result).to contain_exactly(project.ci_project_mirror)
+ end
+ end
+ end
+
describe '.sync!' do
let!(:event) { Projects::SyncEvent.create!(project: project) }
- subject(:sync) { described_class.sync!(event.reload) }
+ subject(:sync) { described_class.sync!(event) }
+
+ context 'when project mirror does not exist in the first place' do
+ before do
+ project.ci_project_mirror.destroy!
+ end
- context 'when project hierarchy does not exist in the first place' do
it 'creates a ci_projects record' do
expect { sync }.to change { described_class.count }.from(0).to(1)
@@ -21,11 +45,7 @@ RSpec.describe Ci::ProjectMirror do
end
end
- context 'when project hierarchy does already exist' do
- before do
- described_class.create!(project_id: project.id, namespace_id: group1.id)
- end
-
+ context 'when project mirror does already exist' do
it 'updates the related ci_projects record' do
expect { sync }.not_to change { described_class.count }
diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb
index aae16157fbf..76e74f3193c 100644
--- a/spec/models/ci/resource_group_spec.rb
+++ b/spec/models/ci/resource_group_spec.rb
@@ -3,6 +3,11 @@
require 'spec_helper'
RSpec.describe Ci::ResourceGroup do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_resource_group, project: parent) }
+ end
+
describe 'validation' do
it 'valids when key includes allowed character' do
resource_group = build(:ci_resource_group, key: 'test')
diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb
index 41d805adb9f..2d1fe11147c 100644
--- a/spec/models/ci/runner_namespace_spec.rb
+++ b/spec/models/ci/runner_namespace_spec.rb
@@ -6,4 +6,10 @@ RSpec.describe Ci::RunnerNamespace do
it_behaves_like 'includes Limitable concern' do
subject { build(:ci_runner_namespace, group: create(:group, :nested), runner: create(:ci_runner, :group)) }
end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:model) { create(:ci_runner_namespace) }
+
+ let!(:parent) { model.namespace }
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 5142f70fa2c..6830a8daa3b 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Ci::Runner do
let(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'disallows assigning group if already assigned to a group' do
- runner.runner_namespaces << build(:ci_runner_namespace)
+ runner.runner_namespaces << create(:ci_runner_namespace)
expect(runner).not_to be_valid
expect(runner.errors.full_messages).to include('Runner needs to be assigned to exactly one group')
@@ -203,28 +203,56 @@ RSpec.describe Ci::Runner do
end
end
- describe '.belonging_to_parent_group_of_project' do
- let(:project) { create(:project, group: group) }
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, groups: [group]) }
- let!(:unrelated_group) { create(:group) }
- let!(:unrelated_project) { create(:project, group: unrelated_group) }
- let!(:unrelated_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
+ shared_examples '.belonging_to_parent_group_of_project' do
+ let!(:group1) { create(:group) }
+ let!(:project1) { create(:project, group: group1) }
+ let!(:runner1) { create(:ci_runner, :group, groups: [group1]) }
+
+ let!(:group2) { create(:group) }
+ let!(:project2) { create(:project, group: group2) }
+ let!(:runner2) { create(:ci_runner, :group, groups: [group2]) }
+
+ let(:project_id) { project1.id }
+
+ subject(:result) { described_class.belonging_to_parent_group_of_project(project_id) }
it 'returns the specific group runner' do
- expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
+ expect(result).to contain_exactly(runner1)
end
- context 'with a parent group with a runner' do
- let(:runner) { create(:ci_runner, :group, groups: [parent_group]) }
- let(:project) { create(:project, group: group) }
- let(:group) { create(:group, parent: parent_group) }
- let(:parent_group) { create(:group) }
+ context 'with a parent group with a runner', :sidekiq_inline do
+ before do
+ group1.update!(parent: group2)
+ end
- it 'returns the group runner from the parent group' do
- expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
+ it 'returns the group runner from the group and the parent group' do
+ expect(result).to contain_exactly(runner1, runner2)
end
end
+
+ context 'with multiple project ids' do
+ let(:project_id) { [project1.id, project2.id] }
+
+ it 'raises ArgumentError' do
+ expect { result }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ context 'when use_traversal_ids* are enabled' do
+ it_behaves_like '.belonging_to_parent_group_of_project'
+ end
+
+ context 'when use_traversal_ids* are disabled' do
+ before do
+ stub_feature_flags(
+ use_traversal_ids: false,
+ use_traversal_ids_for_ancestors: false,
+ use_traversal_ids_for_ancestor_scopes: false
+ )
+ end
+
+ it_behaves_like '.belonging_to_parent_group_of_project'
end
describe '.owned_or_instance_wide' do
@@ -1358,7 +1386,7 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(contacted_at_stored) }
end
- describe '.belonging_to_group' do
+ describe '.legacy_belonging_to_group' do
shared_examples 'returns group runners' do
it 'returns the specific group runner' do
group = create(:group)
@@ -1366,7 +1394,7 @@ RSpec.describe Ci::Runner do
unrelated_group = create(:group)
create(:ci_runner, :group, groups: [unrelated_group])
- expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner)
+ expect(described_class.legacy_belonging_to_group(group.id)).to contain_exactly(runner)
end
context 'runner belonging to parent group' do
@@ -1376,13 +1404,13 @@ RSpec.describe Ci::Runner do
context 'when include_parent option is passed' do
it 'returns the group runner from the parent group' do
- expect(described_class.belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner)
+ expect(described_class.legacy_belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner)
end
end
context 'when include_parent option is not passed' do
it 'does not return the group runner from the parent group' do
- expect(described_class.belonging_to_group(group.id)).to be_empty
+ expect(described_class.legacy_belonging_to_group(group.id)).to be_empty
end
end
end
@@ -1398,4 +1426,48 @@ RSpec.describe Ci::Runner do
it_behaves_like 'returns group runners'
end
end
+
+ describe '.belonging_to_group' do
+ it 'returns the specific group runner' do
+ group = create(:group)
+ runner = create(:ci_runner, :group, groups: [group])
+ unrelated_group = create(:group)
+ create(:ci_runner, :group, groups: [unrelated_group])
+
+ expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner)
+ end
+ end
+
+ describe '.belonging_to_group_and_ancestors' do
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_runner) { create(:ci_runner, :group, groups: [parent_group]) }
+ let_it_be(:group) { create(:group, parent: parent_group) }
+
+ it 'returns the group runner from the parent group' do
+ expect(described_class.belonging_to_group_and_ancestors(group.id)).to contain_exactly(parent_runner)
+ end
+ end
+
+ describe '.belonging_to_group_or_project_descendants' do
+ it 'returns the specific group runners' do
+ group1 = create(:group)
+ group2 = create(:group, parent: group1)
+ group3 = create(:group)
+
+ project1 = create(:project, namespace: group1)
+ project2 = create(:project, namespace: group2)
+ project3 = create(:project, namespace: group3)
+
+ runner1 = create(:ci_runner, :group, groups: [group1])
+ runner2 = create(:ci_runner, :group, groups: [group2])
+ _runner3 = create(:ci_runner, :group, groups: [group3])
+ runner4 = create(:ci_runner, :project, projects: [project1])
+ runner5 = create(:ci_runner, :project, projects: [project2])
+ _runner6 = create(:ci_runner, :project, projects: [project3])
+
+ expect(described_class.belonging_to_group_or_project_descendants(group1.id)).to contain_exactly(
+ runner1, runner2, runner4, runner5
+ )
+ end
+ end
end
diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb
index 629861e35b8..d2f74494308 100644
--- a/spec/models/ci/running_build_spec.rb
+++ b/spec/models/ci/running_build_spec.rb
@@ -49,4 +49,9 @@ RSpec.describe Ci::RunningBuild do
end
end
end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_running_build, project: parent) }
+ end
end
diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb
new file mode 100644
index 00000000000..ae57b63e7a4
--- /dev/null
+++ b/spec/models/ci/secure_file_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::SecureFile do
+ let(:sample_file) { fixture_file('ci_secure_files/upload-keystore.jks') }
+
+ subject { create(:ci_secure_file) }
+
+ before do
+ stub_ci_secure_file_object_storage
+ end
+
+ it { is_expected.to be_a FileStoreMounter }
+
+ it { is_expected.to belong_to(:project).required }
+
+ it_behaves_like 'having unique enum values'
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:checksum) }
+ it { is_expected.to validate_presence_of(:file_store) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_presence_of(:permissions) }
+ it { is_expected.to validate_presence_of(:project_id) }
+ end
+
+ describe '#permissions' do
+ it 'defaults to read_only file permssions' do
+ expect(subject.permissions).to eq('read_only')
+ end
+ end
+
+ describe '#checksum' do
+ it 'computes SHA256 checksum on the file before encrypted' do
+ subject.file = CarrierWaveStringFile.new(sample_file)
+ subject.save!
+ expect(subject.checksum).to eq(Digest::SHA256.hexdigest(sample_file))
+ end
+ end
+
+ describe '#checksum_algorithm' do
+ it 'returns the configured checksum_algorithm' do
+ expect(subject.checksum_algorithm).to eq('sha256')
+ end
+ end
+
+ describe '#file' do
+ it 'returns the saved file' do
+ subject.file = CarrierWaveStringFile.new(sample_file)
+ subject.save!
+ expect(Base64.encode64(subject.file.read)).to eq(Base64.encode64(sample_file))
+ end
+ end
+end
diff --git a/spec/models/ci/unit_test_spec.rb b/spec/models/ci/unit_test_spec.rb
index 2207a362be3..556cf93c266 100644
--- a/spec/models/ci/unit_test_spec.rb
+++ b/spec/models/ci/unit_test_spec.rb
@@ -3,6 +3,11 @@
require 'spec_helper'
RSpec.describe Ci::UnitTest do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_unit_test, project: parent) }
+ end
+
describe 'relationships' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:unit_test_failures) }
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index 3b521086c14..f279e779de5 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -76,12 +76,12 @@ RSpec.describe Clusters::Agent do
end
end
- describe '#active?' do
+ describe '#connected?' do
let_it_be(:agent) { create(:cluster_agent) }
let!(:token) { create(:cluster_agent_token, agent: agent, last_used_at: last_used_at) }
- subject { agent.active? }
+ subject { agent.connected? }
context 'agent has never connected' do
let(:last_used_at) { nil }
@@ -99,6 +99,14 @@ RSpec.describe Clusters::Agent do
let(:last_used_at) { 2.minutes.ago }
it { is_expected.to be_truthy }
+
+ context 'agent token has been revoked' do
+ before do
+ token.revoked!
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
context 'agent has multiple tokens' do
@@ -108,4 +116,19 @@ RSpec.describe Clusters::Agent do
it { is_expected.to be_truthy }
end
end
+
+ describe '#activity_event_deletion_cutoff' do
+ let_it_be(:agent) { create(:cluster_agent) }
+ let_it_be(:event1) { create(:agent_activity_event, agent: agent, recorded_at: 1.hour.ago) }
+ let_it_be(:event2) { create(:agent_activity_event, agent: agent, recorded_at: 2.hours.ago) }
+ let_it_be(:event3) { create(:agent_activity_event, agent: agent, recorded_at: 3.hours.ago) }
+
+ subject { agent.activity_event_deletion_cutoff }
+
+ before do
+ stub_const("#{described_class}::ACTIVITY_EVENT_LIMIT", 2)
+ end
+
+ it { is_expected.to be_like_time(event2.recorded_at) }
+ end
end
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index ad9f948224f..efa2a3eb09b 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -9,17 +9,29 @@ RSpec.describe Clusters::AgentToken do
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:name) }
+ it_behaves_like 'having unique enum values'
+
describe 'scopes' do
describe '.order_last_used_at_desc' do
- let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) }
- let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) }
- let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) }
+ let_it_be(:agent) { create(:cluster_agent) }
+ let_it_be(:token_1) { create(:cluster_agent_token, agent: agent, last_used_at: 7.days.ago) }
+ let_it_be(:token_2) { create(:cluster_agent_token, agent: agent, last_used_at: nil) }
+ let_it_be(:token_3) { create(:cluster_agent_token, agent: agent, last_used_at: 2.days.ago) }
it 'sorts by last_used_at descending, with null values at last' do
expect(described_class.order_last_used_at_desc)
.to eq([token_3, token_1, token_2])
end
end
+
+ describe '.with_status' do
+ let!(:active_token) { create(:cluster_agent_token) }
+ let!(:revoked_token) { create(:cluster_agent_token, :revoked) }
+
+ subject { described_class.with_status(:active) }
+
+ it { is_expected.to contain_exactly(active_token) }
+ end
end
describe '#token' do
@@ -37,83 +49,4 @@ RSpec.describe Clusters::AgentToken do
expect(agent_token.token.length).to be >= 50
end
end
-
- describe '#track_usage', :clean_gitlab_redis_cache do
- let_it_be(:agent) { create(:cluster_agent) }
-
- let(:agent_token) { create(:cluster_agent_token, agent: agent) }
-
- subject { agent_token.track_usage }
-
- context 'when last_used_at was updated recently' do
- before do
- agent_token.update!(last_used_at: 10.minutes.ago)
- end
-
- it 'updates cache but not database' do
- expect { subject }.not_to change { agent_token.reload.read_attribute(:last_used_at) }
-
- expect_redis_update
- end
- end
-
- context 'when last_used_at was not updated recently' do
- it 'updates cache and database' do
- does_db_update
- expect_redis_update
- end
-
- context 'with invalid token' do
- before do
- agent_token.description = SecureRandom.hex(2000)
- end
-
- it 'still updates caches and database' do
- expect(agent_token).to be_invalid
-
- does_db_update
- expect_redis_update
- end
- end
-
- context 'agent is inactive' do
- before do
- allow(agent).to receive(:active?).and_return(false)
- end
-
- it 'creates an activity event' do
- expect { subject }.to change { agent.activity_events.count }
-
- event = agent.activity_events.last
- expect(event).to have_attributes(
- kind: 'agent_connected',
- level: 'info',
- recorded_at: agent_token.reload.read_attribute(:last_used_at),
- agent_token: agent_token
- )
- end
- end
-
- context 'agent is active' do
- before do
- allow(agent).to receive(:active?).and_return(true)
- end
-
- it 'does not create an activity event' do
- expect { subject }.not_to change { agent.activity_events.count }
- end
- end
- end
-
- def expect_redis_update
- Gitlab::Redis::Cache.with do |redis|
- redis_key = "cache:#{described_class.name}:#{agent_token.id}:attributes"
- expect(redis.get(redis_key)).to be_present
- end
- end
-
- def does_db_update
- expect { subject }.to change { agent_token.reload.read_attribute(:last_used_at) }
- end
- end
end
diff --git a/spec/models/clusters/agents/activity_event_spec.rb b/spec/models/clusters/agents/activity_event_spec.rb
index 18b9c82fa6a..2e3833898fd 100644
--- a/spec/models/clusters/agents/activity_event_spec.rb
+++ b/spec/models/clusters/agents/activity_event_spec.rb
@@ -16,11 +16,10 @@ RSpec.describe Clusters::Agents::ActivityEvent do
let_it_be(:agent) { create(:cluster_agent) }
describe '.in_timeline_order' do
- let(:recorded_at) { 1.hour.ago }
-
- let!(:event1) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
- let!(:event2) { create(:agent_activity_event, agent: agent, recorded_at: Time.current) }
- let!(:event3) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
+ let_it_be(:recorded_at) { 1.hour.ago }
+ let_it_be(:event1) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
+ let_it_be(:event2) { create(:agent_activity_event, agent: agent, recorded_at: Time.current) }
+ let_it_be(:event3) { create(:agent_activity_event, agent: agent, recorded_at: recorded_at) }
subject { described_class.in_timeline_order }
@@ -28,5 +27,19 @@ RSpec.describe Clusters::Agents::ActivityEvent do
is_expected.to eq([event2, event3, event1])
end
end
+
+ describe '.recorded_before' do
+ let_it_be(:event1) { create(:agent_activity_event, agent: agent, recorded_at: 1.hour.ago) }
+ let_it_be(:event2) { create(:agent_activity_event, agent: agent, recorded_at: 2.hours.ago) }
+ let_it_be(:event3) { create(:agent_activity_event, agent: agent, recorded_at: 3.hours.ago) }
+
+ let(:cutoff) { event2.recorded_at }
+
+ subject { described_class.recorded_before(cutoff) }
+
+ it 'returns only events recorded before the cutoff' do
+ is_expected.to contain_exactly(event3)
+ end
+ end
end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 434d7ad4a90..8f02161843b 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -101,19 +101,6 @@ RSpec.describe Clusters::Applications::Runner do
end
end
- describe '#prepare_uninstall' do
- it 'pauses associated runner' do
- active_runner = create(:ci_runner, contacted_at: 1.second.ago)
-
- expect(active_runner.active).to be_truthy
-
- application_runner = create(:clusters_applications_runner, :scheduled, runner: active_runner)
- application_runner.prepare_uninstall
-
- expect(active_runner.active).to be_falsey
- end
- end
-
describe '#make_uninstalling!' do
subject { create(:clusters_applications_runner, :scheduled, runner: ci_runner) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 665a2a936af..d5e74d36b58 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -46,28 +46,10 @@ RSpec.describe CommitStatus do
describe 'status state machine' do
let!(:commit_status) { create(:commit_status, :running, project: project) }
- context 'when expire_job_and_pipeline_cache_synchronously is enabled' do
- before do
- stub_feature_flags(expire_job_and_pipeline_cache_synchronously: true)
- end
-
- it 'invalidates the cache after a transition' do
- expect(commit_status).to receive(:expire_etag_cache!)
+ it 'invalidates the cache after a transition' do
+ expect(commit_status).to receive(:expire_etag_cache!)
- commit_status.success!
- end
- end
-
- context 'when expire_job_and_pipeline_cache_synchronously is disabled' do
- before do
- stub_feature_flags(expire_job_and_pipeline_cache_synchronously: false)
- end
-
- it 'invalidates the cache after a transition' do
- expect(ExpireJobCacheWorker).to receive(:perform_async).with(commit_status.id)
-
- commit_status.success!
- end
+ commit_status.success!
end
describe 'transitioning to running' do
@@ -773,6 +755,26 @@ RSpec.describe CommitStatus do
expect { commit_status.drop! }.to change { commit_status.status }.from('manual').to('failed')
end
end
+
+ context 'when a failure reason is provided' do
+ context 'when a failure reason is a symbol' do
+ it 'correctly sets a failure reason' do
+ commit_status.drop!(:script_failure)
+
+ expect(commit_status).to be_script_failure
+ end
+ end
+
+ context 'when a failure reason is an object' do
+ it 'correctly sets a failure reason' do
+ reason = ::Gitlab::Ci::Build::Status::Reason.new(commit_status, :script_failure)
+
+ commit_status.drop!(reason)
+
+ expect(commit_status).to be_script_failure
+ end
+ end
+ end
end
describe 'ensure stage assignment' do
@@ -961,18 +963,17 @@ RSpec.describe CommitStatus do
describe '.bulk_insert_tags!' do
let(:statuses) { double('statuses') }
- let(:tag_list_by_build) { double('tag list') }
let(:inserter) { double('inserter') }
it 'delegates to bulk insert class' do
expect(Gitlab::Ci::Tags::BulkInsert)
.to receive(:new)
- .with(statuses, tag_list_by_build)
+ .with(statuses)
.and_return(inserter)
expect(inserter).to receive(:insert!)
- described_class.bulk_insert_tags!(statuses, tag_list_by_build)
+ described_class.bulk_insert_tags!(statuses)
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 2a3f639a8ac..e9c3d1dc646 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -922,6 +922,22 @@ RSpec.describe Issuable do
end
end
+ describe '#supports_escalation?' do
+ where(:issuable_type, :supports_escalation) do
+ :issue | false
+ :incident | true
+ :merge_request | false
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ subject { issuable.supports_escalation? }
+
+ it { is_expected.to eq(supports_escalation) }
+ end
+ end
+
describe '#incident?' do
where(:issuable_type, :incident) do
:issue | false
diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb
index 50cf7377b99..99a3a0fb79a 100644
--- a/spec/models/concerns/participable_spec.rb
+++ b/spec/models/concerns/participable_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe Participable do
allow(instance).to receive_message_chain(:model_name, :element) { 'class' }
expect(instance).to receive(:foo).and_return(user2)
expect(instance).to receive(:bar).and_return(user3)
- expect(instance).to receive(:project).thrice.and_return(project)
+ expect(instance).to receive(:project).twice.and_return(project)
participants = instance.visible_participants(user1)
@@ -159,31 +159,10 @@ RSpec.describe Participable do
allow(instance).to receive_message_chain(:model_name, :element) { 'class' }
allow(instance).to receive(:bar).and_return(user2)
- expect(instance).to receive(:project).thrice.and_return(project)
+ expect(instance).to receive(:project).twice.and_return(project)
expect(instance.visible_participants(user1)).to be_empty
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(verify_participants_access: false)
- end
-
- it 'returns unavailable participants' do
- model.participant(:bar)
-
- instance = model.new
- user1 = build(:user)
- user2 = build(:user)
- project = build(:project, :public)
-
- allow(instance).to receive_message_chain(:model_name, :element) { 'class' }
- allow(instance).to receive(:bar).and_return(user2)
- expect(instance).to receive(:project).thrice.and_return(project)
-
- expect(instance.visible_participants(user1)).to match_array([user2])
- end
- end
end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 2330147b376..cf66ba83e87 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -141,6 +141,11 @@ RSpec.describe Group, 'Routable', :with_clean_rails_cache do
end
end
+ it 'creates route with namespace referencing group' do
+ expect(group.route).not_to be_nil
+ expect(group.route.namespace).to eq(group)
+ end
+
describe '.where_full_path_in' do
context 'without any paths' do
it 'returns an empty relation' do
@@ -208,30 +213,20 @@ RSpec.describe Project, 'Routable', :with_clean_rails_cache do
it_behaves_like 'routable resource with parent' do
let_it_be(:record) { project }
end
+
+ it 'creates route with namespace referencing project namespace' do
+ expect(project.route).not_to be_nil
+ expect(project.route.namespace).to eq(project.project_namespace)
+ end
end
RSpec.describe Namespaces::ProjectNamespace, 'Routable', :with_clean_rails_cache do
let_it_be(:group) { create(:group) }
- let_it_be(:project_namespace) do
- # For now we create only project namespace w/o project, otherwise same path
- # would be used for project and project namespace.
- # This can be removed when route is created automatically for project namespaces.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/346448
- create(:project_namespace, project: nil, parent: group,
- visibility_level: Gitlab::VisibilityLevel::PUBLIC,
- path: 'foo', name: 'foo').tap do |project_namespace|
- Route.create!(source: project_namespace, path: project_namespace.full_path,
- name: project_namespace.full_name)
- end
- end
-
- # we have couple of places where we use generic Namespace, in that case
- # we don't want to include ProjectNamespace routes yet
- it 'ignores project namespace when searching for generic namespace' do
- redirect_route = create(:redirect_route, source: project_namespace)
- expect(Namespace.find_by_full_path(project_namespace.full_path)).to be_nil
- expect(Namespace.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
+ it 'skips route creation for the resource' do
+ expect do
+ described_class.create!(project: nil, parent: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC, path: 'foo', name: 'foo')
+ end.not_to change { Route.count }
end
end
diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb
index 10a6c1aa821..90c88c888ff 100644
--- a/spec/models/concerns/triggerable_hooks_spec.rb
+++ b/spec/models/concerns/triggerable_hooks_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe TriggerableHooks do
describe '.select_active' do
it 'returns hooks that match the active filter' do
TestableHook.create!(url: 'http://example1.com', push_events: true)
- TestableHook.create!(url: 'http://example2.com', push_events: true)
+ TestableHook.create!(url: 'http://example.org', push_events: true)
filter1 = double(:filter1)
filter2 = double(:filter2)
allow(ActiveHookFilter).to receive(:new).twice.and_return(filter1, filter2)
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 51fdbfebd3a..8f7c13d7ae6 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -25,12 +25,20 @@ RSpec.describe ContainerRepository do
headers: { 'Content-Type' => 'application/json' })
end
+ it_behaves_like 'having unique enum values'
+
describe 'associations' do
it 'belongs to the project' do
expect(repository).to belong_to(:project)
end
end
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:migration_retries_count) }
+ it { is_expected.to validate_numericality_of(:migration_retries_count).is_greater_than_or_equal_to(0) }
+ it { is_expected.to validate_presence_of(:migration_state) }
+ end
+
describe '#tag' do
it 'has a test tag' do
expect(repository.tag('test')).not_to be_nil
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 7e26d324ac2..1225f9d089b 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -26,6 +26,38 @@ RSpec.describe CustomerRelations::Contact, type: :model do
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
end
+ describe '#unique_email_for_group_hierarchy' do
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+
+ let_it_be(:existing_contact) { create(:contact, group: group) }
+
+ context 'with unique email for group hierarchy' do
+ subject { build(:contact, group: group) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with duplicate email in group' do
+ subject { build(:contact, email: existing_contact.email, group: group) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'with duplicate email in parent group' do
+ subject { build(:contact, email: existing_contact.email, group: subgroup) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'with duplicate email in subgroup' do
+ subject { build(:contact, email: existing_contact.email, group: parent) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
describe '#before_validation' do
it 'strips leading and trailing whitespace' do
contact = described_class.new(first_name: ' First ', last_name: ' Last ', phone: ' 123456 ')
@@ -43,20 +75,27 @@ RSpec.describe CustomerRelations::Contact, type: :model do
let_it_be(:other_contacts) { create_list(:contact, 2) }
it 'returns ids of contacts from group' do
- contact_ids = described_class.find_ids_by_emails(group.id, group_contacts.pluck(:email))
+ contact_ids = described_class.find_ids_by_emails(group, group_contacts.pluck(:email))
+
+ expect(contact_ids).to match_array(group_contacts.pluck(:id))
+ end
+
+ it 'returns ids of contacts from parent group' do
+ subgroup = create(:group, parent: group)
+ contact_ids = described_class.find_ids_by_emails(subgroup, group_contacts.pluck(:email))
expect(contact_ids).to match_array(group_contacts.pluck(:id))
end
it 'does not return ids of contacts from other groups' do
- contact_ids = described_class.find_ids_by_emails(group.id, other_contacts.pluck(:email))
+ contact_ids = described_class.find_ids_by_emails(group, other_contacts.pluck(:email))
expect(contact_ids).to be_empty
end
it 'raises ArgumentError when called with too many emails' do
too_many_emails = described_class::MAX_PLUCK + 1
- expect { described_class.find_ids_by_emails(group.id, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
+ expect { described_class.find_ids_by_emails(group, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
end
end
end
diff --git a/spec/models/customer_relations/issue_contact_spec.rb b/spec/models/customer_relations/issue_contact_spec.rb
index 474455a9884..c6373fddbfb 100644
--- a/spec/models/customer_relations/issue_contact_spec.rb
+++ b/spec/models/customer_relations/issue_contact_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe CustomerRelations::IssueContact do
let_it_be(:issue_contact, reload: true) { create(:issue_customer_relations_contact) }
let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, group: subgroup) }
let_it_be(:issue) { create(:issue, project: project) }
subject { issue_contact }
@@ -33,17 +34,29 @@ RSpec.describe CustomerRelations::IssueContact do
end
it 'builds using the same group', :aggregate_failures do
- expect(for_issue.contact.group).to eq(group)
+ expect(for_issue.contact.group).to eq(subgroup)
expect(for_contact.issue.project.group).to eq(group)
end
end
describe 'validation' do
- let(:built) { build(:issue_customer_relations_contact, issue: create(:issue), contact: create(:contact)) }
+ it 'fails when the contact group does not belong to the issue group or ancestors' do
+ built = build(:issue_customer_relations_contact, issue: create(:issue), contact: create(:contact))
- it 'fails when the contact group does not match the issue group' do
expect(built).not_to be_valid
end
+
+ it 'succeeds when the contact group is the same as the issue group' do
+ built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: subgroup))
+
+ expect(built).to be_valid
+ end
+
+ it 'succeeds when the contact group is an ancestor of the issue group' do
+ built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: group))
+
+ expect(built).to be_valid
+ end
end
describe '#self.find_contact_ids_by_emails' do
diff --git a/spec/models/dependency_proxy/blob_spec.rb b/spec/models/dependency_proxy/blob_spec.rb
index 3c54d3126a8..10d06406ad7 100644
--- a/spec/models/dependency_proxy/blob_spec.rb
+++ b/spec/models/dependency_proxy/blob_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
RSpec.describe DependencyProxy::Blob, type: :model do
it_behaves_like 'ttl_expirable'
+ it_behaves_like 'destructible', factory: :dependency_proxy_blob
describe 'relationships' do
it { is_expected.to belong_to(:group) }
diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb
index 59415096989..ab7881b1d39 100644
--- a/spec/models/dependency_proxy/manifest_spec.rb
+++ b/spec/models/dependency_proxy/manifest_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
RSpec.describe DependencyProxy::Manifest, type: :model do
it_behaves_like 'ttl_expirable'
+ it_behaves_like 'destructible', factory: :dependency_proxy_manifest
describe 'relationships' do
it { is_expected.to belong_to(:group) }
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 59299a507e4..b76063bfa1a 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Email do
end
describe 'validations' do
- it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email do
+ it_behaves_like 'an object with email-formatted attributes', :email do
subject { build(:email) }
end
@@ -71,4 +71,84 @@ RSpec.describe Email do
end
end
end
+
+ describe '#confirm' do
+ let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at) { Date.today }
+
+ let(:email) do
+ create(:email, email: 'test@gitlab.com').tap do |email|
+ email.update!(confirmation_sent_at: confirmation_sent_at)
+ end
+ end
+
+ shared_examples_for 'unconfirmed email' do
+ it 'returns unconfirmed' do
+ expect(email.confirmed?).to be_falsey
+ end
+ end
+
+ context 'when the confirmation period has expired' do
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed email'
+
+ it 'does not confirm the email' do
+ email.confirm
+
+ expect(email.confirmed?).to be_falsey
+ end
+ end
+
+ context 'when the confirmation period has not expired' do
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed email'
+
+ it 'confirms the email' do
+ email.confirm
+
+ expect(email.confirmed?).to be_truthy
+ end
+ end
+ end
+
+ describe '#force_confirm' do
+ let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at) { Date.today }
+
+ let(:email) do
+ create(:email, email: 'test@gitlab.com').tap do |email|
+ email.update!(confirmation_sent_at: confirmation_sent_at)
+ end
+ end
+
+ shared_examples_for 'unconfirmed email' do
+ it 'returns unconfirmed' do
+ expect(email.confirmed?).to be_falsey
+ end
+ end
+
+ shared_examples_for 'confirms the email on force_confirm' do
+ it 'confirms an email' do
+ email.force_confirm
+
+ expect(email.reload.confirmed?).to be_truthy
+ end
+ end
+
+ context 'when the confirmation period has expired' do
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed email'
+ it_behaves_like 'confirms the email on force_confirm'
+ end
+
+ context 'when the confirmation period has not expired' do
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed email'
+ it_behaves_like 'confirms the email on force_confirm'
+ end
+ end
end
diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb
index ea5d2b27028..de6ce3ba053 100644
--- a/spec/models/experiment_spec.rb
+++ b/spec/models/experiment_spec.rb
@@ -235,6 +235,54 @@ RSpec.describe Experiment do
end
end
+ describe '#record_conversion_event_for_subject' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:experiment) { create(:experiment) }
+ let_it_be(:context) { { a: 42 } }
+
+ subject(:record_conversion) { experiment.record_conversion_event_for_subject(user, context) }
+
+ context 'when no existing experiment_subject record exists for the given user' do
+ it 'does not update or create an experiment_subject record' do
+ expect { record_conversion }.not_to change { ExperimentSubject.all.to_a }
+ end
+ end
+
+ context 'when an existing experiment_subject exists for the given user' do
+ context 'but it has already been converted' do
+ let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago) }
+
+ it 'does not update the converted_at value' do
+ expect { record_conversion }.not_to change { experiment_subject.converted_at }
+ end
+ end
+
+ context 'and it has not yet been converted' do
+ let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
+
+ it 'updates the converted_at value' do
+ expect { record_conversion }.to change { experiment_subject.reload.converted_at }
+ end
+ end
+
+ context 'with no existing context' do
+ let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
+
+ it 'updates the context' do
+ expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42)
+ end
+ end
+
+ context 'with an existing context' do
+ let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago, context: { b: 1 } ) }
+
+ it 'merges the context' do
+ expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42, 'b' => 1)
+ end
+ end
+ end
+ end
+
describe '#record_subject_and_variant!' do
let_it_be(:subject_to_record) { create(:group) }
let_it_be(:variant) { :control }
diff --git a/spec/models/group/crm_settings_spec.rb b/spec/models/group/crm_settings_spec.rb
new file mode 100644
index 00000000000..35fcdca6389
--- /dev/null
+++ b/spec/models/group/crm_settings_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Group::CrmSettings do
+ describe 'associations' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ subject { build(:crm_settings) }
+
+ it { is_expected.to validate_presence_of(:group) }
+ end
+end
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
index 03cc9d7e64c..034a5c1dfc6 100644
--- a/spec/models/group_group_link_spec.rb
+++ b/spec/models/group_group_link_spec.rb
@@ -29,32 +29,6 @@ RSpec.describe GroupGroupLink do
])
end
end
-
- describe '.public_or_visible_to_user' do
- let!(:user_with_access) { create :user }
- let!(:user_without_access) { create :user }
- let!(:shared_with_group) { create :group, :private }
- let!(:shared_group) { create :group }
- let!(:private_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_group) }
-
- before do
- shared_group.add_owner(user_with_access)
- shared_group.add_owner(user_without_access)
- shared_with_group.add_developer(user_with_access)
- end
-
- context 'when user can access shared group' do
- it 'returns the private group' do
- expect(described_class.public_or_visible_to_user(shared_group, user_with_access)).to include(private_group_group_link)
- end
- end
-
- context 'when user does not have access to shared group' do
- it 'does not return private group' do
- expect(described_class.public_or_visible_to_user(shared_group, user_without_access)).not_to include(private_group_group_link)
- end
- end
- end
end
describe 'validation' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index fed4ee3f3a4..05ee2166245 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Group do
include ReloadHelpers
+ include StubGitlabCalls
let!(:group) { create(:group) }
@@ -39,6 +40,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
+ it { is_expected.to have_one(:crm_settings) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -63,6 +65,7 @@ RSpec.describe Group do
describe 'validations' do
it { is_expected.to validate_presence_of :name }
+ it { is_expected.not_to allow_value('colon:in:path').for(:path) } # This is to validate that a specially crafted name cannot bypass a pattern match. See !72555
it { is_expected.to allow_value('group test_4').for(:name) }
it { is_expected.not_to allow_value('test/../foo').for(:name) }
it { is_expected.not_to allow_value('<script>alert("Attack!")</script>').for(:name) }
@@ -502,6 +505,10 @@ RSpec.describe Group do
it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' }
end
+ describe '#self_and_hierarchy' do
+ it { expect(group.self_and_hierarchy.to_sql).not_to include 'traversal_ids @>' }
+ end
+
describe '#ancestors' do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
@@ -526,6 +533,10 @@ RSpec.describe Group do
it { expect(group.descendants.to_sql).to include 'traversal_ids @>' }
end
+ describe '#self_and_hierarchy' do
+ it { expect(group.self_and_hierarchy.to_sql).to include 'traversal_ids @>' }
+ end
+
describe '#ancestors' do
it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" }
@@ -670,6 +681,26 @@ RSpec.describe Group do
expect(result).to match_array([internal_group])
end
end
+
+ describe 'by_ids_or_paths' do
+ let(:group_path) { 'group_path' }
+ let!(:group) { create(:group, path: group_path) }
+ let(:group_id) { group.id }
+
+ it 'returns matching records based on paths' do
+ expect(described_class.by_ids_or_paths(nil, [group_path])).to match_array([group])
+ end
+
+ it 'returns matching records based on ids' do
+ expect(described_class.by_ids_or_paths([group_id], nil)).to match_array([group])
+ end
+
+ it 'returns matching records based on both paths and ids' do
+ new_group = create(:group)
+
+ expect(described_class.by_ids_or_paths([new_group.id], [group_path])).to match_array([group, new_group])
+ end
+ end
end
describe '#to_reference' do
@@ -2056,6 +2087,23 @@ RSpec.describe Group do
end
end
+ describe '#bots' do
+ subject { group.bots }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ [project_bot, user].each do |member|
+ group.add_maintainer(member)
+ end
+ end
+
+ it { is_expected.to contain_exactly(project_bot) }
+ it { is_expected.not_to include(user) }
+ end
+
describe '#related_group_ids' do
let(:nested_group) { create(:group, parent: group) }
let(:shared_with_group) { create(:group, parent: group) }
@@ -2492,7 +2540,7 @@ RSpec.describe Group do
end
end
- describe '#default_owner' do
+ describe '#first_owner' do
let(:group) { build(:group) }
context 'the group has owners' do
@@ -2502,7 +2550,7 @@ RSpec.describe Group do
end
it 'is the first owner' do
- expect(group.default_owner)
+ expect(group.first_owner)
.to eq(group.owners.first)
.and be_a(User)
end
@@ -2517,8 +2565,8 @@ RSpec.describe Group do
end
it 'is the first owner of the parent' do
- expect(group.default_owner)
- .to eq(parent.default_owner)
+ expect(group.first_owner)
+ .to eq(parent.first_owner)
.and be_a(User)
end
end
@@ -2529,7 +2577,7 @@ RSpec.describe Group do
end
it 'is the group.owner' do
- expect(group.default_owner)
+ expect(group.first_owner)
.to eq(group.owner)
.and be_a(User)
end
@@ -2775,4 +2823,330 @@ RSpec.describe Group do
end
end
end
+
+ describe '#dependency_proxy_setting' do
+ subject(:setting) { group.dependency_proxy_setting }
+
+ it 'builds a new policy if one does not exist', :aggregate_failures do
+ expect(setting.enabled).to eq(true)
+ expect(setting).not_to be_persisted
+ end
+
+ context 'with existing policy' do
+ before do
+ group.dependency_proxy_setting.update!(enabled: false)
+ end
+
+ it 'returns the policy if it already exists', :aggregate_failures do
+ expect(setting.enabled).to eq(false)
+ expect(setting).to be_persisted
+ end
+ end
+ end
+
+ describe '#crm_enabled?' do
+ it 'returns false where no crm_settings exist' do
+ expect(group.crm_enabled?).to be_falsey
+ end
+
+ it 'returns false where crm_settings.state is disabled' do
+ create(:crm_settings, enabled: false, group: group)
+
+ expect(group.crm_enabled?).to be_falsey
+ end
+
+ it 'returns true where crm_settings.state is enabled' do
+ create(:crm_settings, enabled: true, group: group)
+
+ expect(group.crm_enabled?).to be_truthy
+ end
+ end
+ describe '.get_ids_by_ids_or_paths' do
+ let(:group_path) { 'group_path' }
+ let!(:group) { create(:group, path: group_path) }
+ let(:group_id) { group.id }
+
+ it 'returns ids matching records based on paths' do
+ expect(described_class.get_ids_by_ids_or_paths(nil, [group_path])).to match_array([group_id])
+ end
+
+ it 'returns ids matching records based on ids' do
+ expect(described_class.get_ids_by_ids_or_paths([group_id], nil)).to match_array([group_id])
+ end
+
+ it 'returns ids matching records based on both paths and ids' do
+ new_group_id = create(:group).id
+
+ expect(described_class.get_ids_by_ids_or_paths([new_group_id], [group_path])).to match_array([group_id, new_group_id])
+ end
+ end
+
+ describe '#shared_with_group_links_visible_to_user' do
+ let_it_be(:admin) { create :admin }
+ let_it_be(:normal_user) { create :user }
+ let_it_be(:user_with_access) { create :user }
+ let_it_be(:user_with_parent_access) { create :user }
+ let_it_be(:user_without_access) { create :user }
+ let_it_be(:shared_group) { create :group }
+ let_it_be(:parent_group) { create :group, :private }
+ let_it_be(:shared_with_private_group) { create :group, :private, parent: parent_group }
+ let_it_be(:shared_with_internal_group) { create :group, :internal }
+ let_it_be(:shared_with_public_group) { create :group, :public }
+ let_it_be(:private_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_private_group) }
+ let_it_be(:internal_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_internal_group) }
+ let_it_be(:public_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_public_group) }
+
+ before do
+ shared_with_private_group.add_developer(user_with_access)
+ parent_group.add_developer(user_with_parent_access)
+ end
+
+ context 'when user is admin', :enable_admin_mode do
+ it 'returns all existing shared group links' do
+ expect(shared_group.shared_with_group_links_visible_to_user(admin)).to contain_exactly(private_group_group_link, internal_group_group_link, public_group_group_link)
+ end
+ end
+
+ context 'when user is nil' do
+ it 'returns only link of public shared group' do
+ expect(shared_group.shared_with_group_links_visible_to_user(nil)).to contain_exactly(public_group_group_link)
+ end
+ end
+
+ context 'when user has no access to private shared group' do
+ it 'returns links of internal and public shared groups' do
+ expect(shared_group.shared_with_group_links_visible_to_user(normal_user)).to contain_exactly(internal_group_group_link, public_group_group_link)
+ end
+ end
+
+ context 'when user is member of private shared group' do
+ it 'returns links of private, internal and public shared groups' do
+ expect(shared_group.shared_with_group_links_visible_to_user(user_with_access)).to contain_exactly(private_group_group_link, internal_group_group_link, public_group_group_link)
+ end
+ end
+
+ context 'when user is inherited member of private shared group' do
+ it 'returns links of private, internal and public shared groups' do
+ expect(shared_group.shared_with_group_links_visible_to_user(user_with_parent_access)).to contain_exactly(private_group_group_link, internal_group_group_link, public_group_group_link)
+ end
+ end
+ end
+
+ describe '#enforced_runner_token_expiration_interval and #effective_runner_token_expiration_interval' do
+ shared_examples 'no enforced expiration interval' do
+ it { expect(subject.enforced_runner_token_expiration_interval).to be_nil }
+ end
+
+ shared_examples 'enforced expiration interval' do |enforced_interval:|
+ it { expect(subject.enforced_runner_token_expiration_interval).to eq(enforced_interval) }
+ end
+
+ shared_examples 'no effective expiration interval' do
+ it { expect(subject.effective_runner_token_expiration_interval).to be_nil }
+ end
+
+ shared_examples 'effective expiration interval' do |effective_interval:|
+ it { expect(subject.effective_runner_token_expiration_interval).to eq(effective_interval) }
+ end
+
+ context 'when there is no interval in group settings' do
+ let_it_be(:group) { create(:group) }
+
+ subject { group }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is a group interval' do
+ let(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 3.days.to_i) }
+
+ subject { create(:group, namespace_settings: group_settings) }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ # runner_token_expiration_interval should not affect the expiration interval, only
+ # group_runner_token_expiration_interval should.
+ context 'when there is a site-wide enforced shared interval' do
+ before do
+ stub_application_setting(runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:group) { create(:group) }
+
+ subject { group }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is a site-wide enforced group interval' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:group) { create(:group) }
+
+ subject { group }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
+ it_behaves_like 'effective expiration interval', effective_interval: 5.days
+ end
+
+ # project_runner_token_expiration_interval should not affect the expiration interval, only
+ # group_runner_token_expiration_interval should.
+ context 'when there is a site-wide enforced project interval' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:group) { create(:group) }
+
+ subject { group }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ # runner_token_expiration_interval should not affect the expiration interval, only
+ # subgroup_runner_token_expiration_interval should.
+ context 'when there is a grandparent group enforced group interval' do
+ let_it_be(:grandparent_group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
+ let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ subject { subgroup }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is a grandparent group enforced subgroup interval' do
+ let_it_be(:grandparent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
+ let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ subject { subgroup }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ # project_runner_token_expiration_interval should not affect the expiration interval, only
+ # subgroup_runner_token_expiration_interval should.
+ context 'when there is a grandparent group enforced project interval' do
+ let_it_be(:grandparent_group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
+ let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ subject { subgroup }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is a parent group enforced interval overridden by group interval' do
+ let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 5.days.to_i) }
+ let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
+ let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:subgroup_with_settings) { create(:group, parent: parent_group, namespace_settings: group_settings) }
+
+ subject { subgroup_with_settings }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+
+ it 'has human-readable expiration intervals' do
+ expect(subject.enforced_runner_token_expiration_interval_human_readable).to eq('5d')
+ expect(subject.effective_runner_token_expiration_interval_human_readable).to eq('4d')
+ end
+ end
+
+ context 'when site-wide enforced interval overrides group interval' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 3.days.to_i)
+ end
+
+ let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group_with_settings) { create(:group, namespace_settings: group_settings) }
+
+ subject { group_with_settings }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ context 'when group interval overrides site-wide enforced interval' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group_with_settings) { create(:group, namespace_settings: group_settings) }
+
+ subject { group_with_settings }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ context 'when site-wide enforced interval overrides parent group enforced interval' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 3.days.to_i)
+ end
+
+ let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ subject { subgroup }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ context 'when parent group enforced interval overrides site-wide enforced interval' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ subject { subgroup }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ # Unrelated groups should not affect the expiration interval.
+ context 'when there is an enforced group interval in an unrelated group' do
+ let_it_be(:unrelated_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:unrelated_group) { create(:group, namespace_settings: unrelated_group_settings) }
+ let_it_be(:group) { create(:group) }
+
+ subject { group }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ # Subgroups should not affect the parent group expiration interval.
+ context 'when there is an enforced group interval in a subgroup' do
+ let_it_be(:subgroup_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:subgroup) { create(:group, parent: group, namespace_settings: subgroup_settings) }
+ let_it_be(:group) { create(:group) }
+
+ subject { group }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index f0ee9a613d8..ec2eca96755 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -40,6 +40,15 @@ RSpec.describe ProjectHook do
end
end
+ describe '#parent' do
+ it 'returns the associated project' do
+ project = build(:project)
+ hook = build(:project_hook, project: project)
+
+ expect(hook.parent).to eq(project)
+ end
+ end
+
describe '#application_context' do
let_it_be(:hook) { build(:project_hook) }
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 4ce2e729d89..85f433f5f81 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -31,6 +31,36 @@ RSpec.describe ServiceHook do
end
end
+ describe '#parent' do
+ let(:hook) { build(:service_hook, integration: integration) }
+
+ context 'with a project-level integration' do
+ let(:project) { build(:project) }
+ let(:integration) { build(:integration, project: project) }
+
+ it 'returns the associated project' do
+ expect(hook.parent).to eq(project)
+ end
+ end
+
+ context 'with a group-level integration' do
+ let(:group) { build(:group) }
+ let(:integration) { build(:integration, :group, group: group) }
+
+ it 'returns the associated group' do
+ expect(hook.parent).to eq(group)
+ end
+ end
+
+ context 'with an instance-level integration' do
+ let(:integration) { build(:integration, :instance) }
+
+ it 'returns nil' do
+ expect(hook.parent).to be_nil
+ end
+ end
+ end
+
describe '#application_context' do
let(:hook) { build(:service_hook) }
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 17cb5da977a..89bfb742f5d 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe SystemHook do
let(:project) { create(:project, namespace: user.namespace) }
let(:group) { create(:group) }
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: 'mydummypass' }
+ { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: Gitlab::Password.test_default }
end
before do
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index 698d74abf03..a47bc6a5b6d 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -205,7 +205,8 @@ RSpec.describe InstanceConfiguration do
group_export_limit: 1018,
group_download_export_limit: 1019,
group_import_limit: 1020,
- raw_blob_request_limit: 1021
+ raw_blob_request_limit: 1021,
+ user_email_lookup_limit: 1022
)
end
@@ -228,6 +229,7 @@ RSpec.describe InstanceConfiguration do
expect(rate_limits[:group_export_download]).to eq({ enabled: true, requests_per_period: 1019, period_in_seconds: 60 })
expect(rate_limits[:group_import]).to eq({ enabled: true, requests_per_period: 1020, period_in_seconds: 60 })
expect(rate_limits[:raw_blob]).to eq({ enabled: true, requests_per_period: 1021, period_in_seconds: 60 })
+ expect(rate_limits[:user_email_lookup]).to eq({ enabled: true, requests_per_period: 1022, period_in_seconds: 60 })
end
end
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index de47fb3839a..7bc670302f1 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -33,28 +33,28 @@ RSpec.describe Integration do
end
with_them do
- it 'validates the service' do
- expect(build(:service, project_id: project_id, group_id: group_id, instance: instance).valid?).to eq(valid)
+ it 'validates the integration' do
+ expect(build(:integration, project_id: project_id, group_id: group_id, instance: instance).valid?).to eq(valid)
end
end
- context 'with existing services' do
+ context 'with existing integrations' do
before_all do
- create(:service, :instance)
- create(:service, project: project)
- create(:service, group: group, project: nil)
+ create(:integration, :instance)
+ create(:integration, project: project)
+ create(:integration, group: group, project: nil)
end
- it 'allows only one instance service per type' do
- expect(build(:service, :instance)).to be_invalid
+ it 'allows only one instance integration per type' do
+ expect(build(:integration, :instance)).to be_invalid
end
- it 'allows only one project service per type' do
- expect(build(:service, project: project)).to be_invalid
+ it 'allows only one project integration per type' do
+ expect(build(:integration, project: project)).to be_invalid
end
- it 'allows only one group service per type' do
- expect(build(:service, group: group, project: nil)).to be_invalid
+ it 'allows only one group integration per type' do
+ expect(build(:integration, group: group, project: nil)).to be_invalid
end
end
end
@@ -79,93 +79,85 @@ RSpec.describe Integration do
end
describe '.by_type' do
- let!(:service1) { create(:jira_integration) }
- let!(:service2) { create(:jira_integration) }
- let!(:service3) { create(:redmine_integration) }
+ let!(:integration1) { create(:jira_integration) }
+ let!(:integration2) { create(:jira_integration) }
+ let!(:integration3) { create(:redmine_integration) }
subject { described_class.by_type(type) }
context 'when type is "JiraService"' do
let(:type) { 'JiraService' }
- it { is_expected.to match_array([service1, service2]) }
+ it { is_expected.to match_array([integration1, integration2]) }
end
context 'when type is "RedmineService"' do
let(:type) { 'RedmineService' }
- it { is_expected.to match_array([service3]) }
+ it { is_expected.to match_array([integration3]) }
end
end
describe '.for_group' do
- let!(:service1) { create(:jira_integration, project_id: nil, group_id: group.id) }
- let!(:service2) { create(:jira_integration) }
+ let!(:integration1) { create(:jira_integration, project_id: nil, group_id: group.id) }
+ let!(:integration2) { create(:jira_integration) }
- it 'returns the right group service' do
- expect(described_class.for_group(group)).to match_array([service1])
+ it 'returns the right group integration' do
+ expect(described_class.for_group(group)).to match_array([integration1])
end
end
- describe '.confidential_note_hooks' do
- it 'includes services where confidential_note_events is true' do
- create(:service, active: true, confidential_note_events: true)
+ shared_examples 'hook scope' do |hook_type|
+ describe ".#{hook_type}_hooks" do
+ it "includes services where #{hook_type}_events is true" do
+ create(:integration, active: true, "#{hook_type}_events": true)
- expect(described_class.confidential_note_hooks.count).to eq 1
- end
+ expect(described_class.send("#{hook_type}_hooks").count).to eq 1
+ end
- it 'excludes services where confidential_note_events is false' do
- create(:service, active: true, confidential_note_events: false)
+ it "excludes services where #{hook_type}_events is false" do
+ create(:integration, active: true, "#{hook_type}_events": false)
- expect(described_class.confidential_note_hooks.count).to eq 0
+ expect(described_class.send("#{hook_type}_hooks").count).to eq 0
+ end
end
end
- describe '.alert_hooks' do
- it 'includes services where alert_events is true' do
- create(:service, active: true, alert_events: true)
-
- expect(described_class.alert_hooks.count).to eq 1
- end
-
- it 'excludes services where alert_events is false' do
- create(:service, active: true, alert_events: false)
-
- expect(described_class.alert_hooks.count).to eq 0
- end
- end
+ include_examples 'hook scope', 'confidential_note'
+ include_examples 'hook scope', 'alert'
+ include_examples 'hook scope', 'archive_trace'
end
describe '#operating?' do
- it 'is false when the service is not active' do
- expect(build(:service).operating?).to eq(false)
+ it 'is false when the integration is not active' do
+ expect(build(:integration).operating?).to eq(false)
end
- it 'is false when the service is not persisted' do
- expect(build(:service, active: true).operating?).to eq(false)
+ it 'is false when the integration is not persisted' do
+ expect(build(:integration, active: true).operating?).to eq(false)
end
- it 'is true when the service is active and persisted' do
- expect(create(:service, active: true).operating?).to eq(true)
+ it 'is true when the integration is active and persisted' do
+ expect(create(:integration, active: true).operating?).to eq(true)
end
end
describe '#testable?' do
context 'when integration is project-level' do
- subject { build(:service, project: project) }
+ subject { build(:integration, project: project) }
it { is_expected.to be_testable }
end
context 'when integration is not project-level' do
- subject { build(:service, project: nil) }
+ subject { build(:integration, project: nil) }
it { is_expected.not_to be_testable }
end
end
describe '#test' do
- let(:integration) { build(:service, project: project) }
+ let(:integration) { build(:integration, project: project) }
let(:data) { 'test' }
it 'calls #execute' do
@@ -186,32 +178,32 @@ RSpec.describe Integration do
end
describe '#project_level?' do
- it 'is true when service has a project' do
- expect(build(:service, project: project)).to be_project_level
+ it 'is true when integration has a project' do
+ expect(build(:integration, project: project)).to be_project_level
end
- it 'is false when service has no project' do
- expect(build(:service, project: nil)).not_to be_project_level
+ it 'is false when integration has no project' do
+ expect(build(:integration, project: nil)).not_to be_project_level
end
end
describe '#group_level?' do
- it 'is true when service has a group' do
- expect(build(:service, group: group)).to be_group_level
+ it 'is true when integration has a group' do
+ expect(build(:integration, group: group)).to be_group_level
end
- it 'is false when service has no group' do
- expect(build(:service, group: nil)).not_to be_group_level
+ it 'is false when integration has no group' do
+ expect(build(:integration, group: nil)).not_to be_group_level
end
end
describe '#instance_level?' do
- it 'is true when service has instance-level integration' do
- expect(build(:service, :instance)).to be_instance_level
+ it 'is true when integration has instance-level integration' do
+ expect(build(:integration, :instance)).to be_instance_level
end
- it 'is false when service does not have instance-level integration' do
- expect(build(:service, instance: false)).not_to be_instance_level
+ it 'is false when integration does not have instance-level integration' do
+ expect(build(:integration, instance: false)).not_to be_instance_level
end
end
@@ -231,19 +223,19 @@ RSpec.describe Integration do
end
describe '.find_or_initialize_all_non_project_specific' do
- shared_examples 'service instances' do
- it 'returns the available service instances' do
+ shared_examples 'integration instances' do
+ it 'returns the available integration instances' do
expect(Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).map(&:to_param))
.to match_array(Integration.available_integration_names(include_project_specific: false))
end
- it 'does not create service instances' do
+ it 'does not create integration instances' do
expect { Integration.find_or_initialize_all_non_project_specific(Integration.for_instance) }
.not_to change(Integration, :count)
end
end
- it_behaves_like 'service instances'
+ it_behaves_like 'integration instances'
context 'with all existing instances' do
before do
@@ -252,15 +244,15 @@ RSpec.describe Integration do
)
end
- it_behaves_like 'service instances'
+ it_behaves_like 'integration instances'
- context 'with a previous existing service (MockCiService) and a new service (Asana)' do
+ context 'with a previous existing integration (MockCiService) and a new integration (Asana)' do
before do
Integration.insert({ type: 'MockCiService', instance: true })
Integration.delete_by(type: 'AsanaService', instance: true)
end
- it_behaves_like 'service instances'
+ it_behaves_like 'integration instances'
end
end
@@ -269,7 +261,7 @@ RSpec.describe Integration do
create(:jira_integration, :instance)
end
- it_behaves_like 'service instances'
+ it_behaves_like 'integration instances'
end
end
@@ -320,31 +312,31 @@ RSpec.describe Integration do
}
end
- shared_examples 'service creation from an integration' do
- it 'creates a correct service for a project integration' do
- service = described_class.build_from_integration(integration, project_id: project.id)
+ shared_examples 'integration creation from an integration' do
+ it 'creates a correct integration for a project integration' do
+ new_integration = described_class.build_from_integration(integration, project_id: project.id)
- expect(service).to be_active
- expect(service.url).to eq(url)
- expect(service.api_url).to eq(api_url)
- expect(service.username).to eq(username)
- expect(service.password).to eq(password)
- expect(service.instance).to eq(false)
- expect(service.project).to eq(project)
- expect(service.group).to eq(nil)
+ expect(new_integration).to be_active
+ expect(new_integration.url).to eq(url)
+ expect(new_integration.api_url).to eq(api_url)
+ expect(new_integration.username).to eq(username)
+ expect(new_integration.password).to eq(password)
+ expect(new_integration.instance).to eq(false)
+ expect(new_integration.project).to eq(project)
+ expect(new_integration.group).to eq(nil)
end
- it 'creates a correct service for a group integration' do
- service = described_class.build_from_integration(integration, group_id: group.id)
-
- expect(service).to be_active
- expect(service.url).to eq(url)
- expect(service.api_url).to eq(api_url)
- expect(service.username).to eq(username)
- expect(service.password).to eq(password)
- expect(service.instance).to eq(false)
- expect(service.project).to eq(nil)
- expect(service.group).to eq(group)
+ it 'creates a correct integration for a group integration' do
+ new_integration = described_class.build_from_integration(integration, group_id: group.id)
+
+ expect(new_integration).to be_active
+ expect(new_integration.url).to eq(url)
+ expect(new_integration.api_url).to eq(api_url)
+ expect(new_integration.username).to eq(username)
+ expect(new_integration.password).to eq(password)
+ expect(new_integration.instance).to eq(false)
+ expect(new_integration.project).to eq(nil)
+ expect(new_integration.group).to eq(group)
end
end
@@ -355,7 +347,7 @@ RSpec.describe Integration do
create(:jira_integration, :without_properties_callback, properties: properties.merge(additional: 'something'))
end
- it_behaves_like 'service creation from an integration'
+ it_behaves_like 'integration creation from an integration'
end
context 'when data are stored in separated fields' do
@@ -363,7 +355,7 @@ RSpec.describe Integration do
create(:jira_integration, data_params.merge(properties: {}))
end
- it_behaves_like 'service creation from an integration'
+ it_behaves_like 'integration creation from an integration'
end
context 'when data are stored in both properties and separated fields' do
@@ -374,7 +366,7 @@ RSpec.describe Integration do
end
end
- it_behaves_like 'service creation from an integration'
+ it_behaves_like 'integration creation from an integration'
end
end
end
@@ -565,17 +557,17 @@ RSpec.describe Integration do
end
describe '.integration_name_to_model' do
- it 'returns the model for the given service name' do
+ it 'returns the model for the given integration name' do
expect(described_class.integration_name_to_model('asana')).to eq(Integrations::Asana)
end
- it 'raises an error if service name is invalid' do
+ it 'raises an error if integration name is invalid' do
expect { described_class.integration_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/)
end
end
describe "{property}_changed?" do
- let(:service) do
+ let(:integration) do
Integrations::Bamboo.create!(
project: project,
properties: {
@@ -587,35 +579,35 @@ RSpec.describe Integration do
end
it "returns false when the property has not been assigned a new value" do
- service.username = "key_changed"
- expect(service.bamboo_url_changed?).to be_falsy
+ integration.username = "key_changed"
+ expect(integration.bamboo_url_changed?).to be_falsy
end
it "returns true when the property has been assigned a different value" do
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_changed?).to be_truthy
+ integration.bamboo_url = "http://example.com"
+ expect(integration.bamboo_url_changed?).to be_truthy
end
it "returns true when the property has been assigned a different value twice" do
- service.bamboo_url = "http://example.com"
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_changed?).to be_truthy
+ integration.bamboo_url = "http://example.com"
+ integration.bamboo_url = "http://example.com"
+ expect(integration.bamboo_url_changed?).to be_truthy
end
it "returns false when the property has been re-assigned the same value" do
- service.bamboo_url = 'http://gitlab.com'
- expect(service.bamboo_url_changed?).to be_falsy
+ integration.bamboo_url = 'http://gitlab.com'
+ expect(integration.bamboo_url_changed?).to be_falsy
end
it "returns false when the property has been assigned a new value then saved" do
- service.bamboo_url = 'http://example.com'
- service.save!
- expect(service.bamboo_url_changed?).to be_falsy
+ integration.bamboo_url = 'http://example.com'
+ integration.save!
+ expect(integration.bamboo_url_changed?).to be_falsy
end
end
describe "{property}_touched?" do
- let(:service) do
+ let(:integration) do
Integrations::Bamboo.create!(
project: project,
properties: {
@@ -627,35 +619,35 @@ RSpec.describe Integration do
end
it "returns false when the property has not been assigned a new value" do
- service.username = "key_changed"
- expect(service.bamboo_url_touched?).to be_falsy
+ integration.username = "key_changed"
+ expect(integration.bamboo_url_touched?).to be_falsy
end
it "returns true when the property has been assigned a different value" do
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_touched?).to be_truthy
+ integration.bamboo_url = "http://example.com"
+ expect(integration.bamboo_url_touched?).to be_truthy
end
it "returns true when the property has been assigned a different value twice" do
- service.bamboo_url = "http://example.com"
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_touched?).to be_truthy
+ integration.bamboo_url = "http://example.com"
+ integration.bamboo_url = "http://example.com"
+ expect(integration.bamboo_url_touched?).to be_truthy
end
it "returns true when the property has been re-assigned the same value" do
- service.bamboo_url = 'http://gitlab.com'
- expect(service.bamboo_url_touched?).to be_truthy
+ integration.bamboo_url = 'http://gitlab.com'
+ expect(integration.bamboo_url_touched?).to be_truthy
end
it "returns false when the property has been assigned a new value then saved" do
- service.bamboo_url = 'http://example.com'
- service.save!
- expect(service.bamboo_url_changed?).to be_falsy
+ integration.bamboo_url = 'http://example.com'
+ integration.save!
+ expect(integration.bamboo_url_changed?).to be_falsy
end
end
describe "{property}_was" do
- let(:service) do
+ let(:integration) do
Integrations::Bamboo.create!(
project: project,
properties: {
@@ -667,35 +659,35 @@ RSpec.describe Integration do
end
it "returns nil when the property has not been assigned a new value" do
- service.username = "key_changed"
- expect(service.bamboo_url_was).to be_nil
+ integration.username = "key_changed"
+ expect(integration.bamboo_url_was).to be_nil
end
it "returns the previous value when the property has been assigned a different value" do
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ integration.bamboo_url = "http://example.com"
+ expect(integration.bamboo_url_was).to eq('http://gitlab.com')
end
it "returns initial value when the property has been re-assigned the same value" do
- service.bamboo_url = 'http://gitlab.com'
- expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ integration.bamboo_url = 'http://gitlab.com'
+ expect(integration.bamboo_url_was).to eq('http://gitlab.com')
end
it "returns initial value when the property has been assigned multiple values" do
- service.bamboo_url = "http://example.com"
- service.bamboo_url = "http://example2.com"
- expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ integration.bamboo_url = "http://example.com"
+ integration.bamboo_url = "http://example.org"
+ expect(integration.bamboo_url_was).to eq('http://gitlab.com')
end
it "returns nil when the property has been assigned a new value then saved" do
- service.bamboo_url = 'http://example.com'
- service.save!
- expect(service.bamboo_url_was).to be_nil
+ integration.bamboo_url = 'http://example.com'
+ integration.save!
+ expect(integration.bamboo_url_was).to be_nil
end
end
- describe 'initialize service with no properties' do
- let(:service) do
+ describe 'initialize integration with no properties' do
+ let(:integration) do
Integrations::Bugzilla.create!(
project: project,
project_url: 'http://gitlab.example.com'
@@ -703,16 +695,16 @@ RSpec.describe Integration do
end
it 'does not raise error' do
- expect { service }.not_to raise_error
+ expect { integration }.not_to raise_error
end
it 'sets data correctly' do
- expect(service.data_fields.project_url).to eq('http://gitlab.example.com')
+ expect(integration.data_fields.project_url).to eq('http://gitlab.example.com')
end
end
describe '#api_field_names' do
- let(:fake_service) do
+ let(:fake_integration) do
Class.new(Integration) do
def fields
[
@@ -728,8 +720,8 @@ RSpec.describe Integration do
end
end
- let(:service) do
- fake_service.new(properties: [
+ let(:integration) do
+ fake_integration.new(properties: [
{ token: 'token-value' },
{ api_token: 'api_token-value' },
{ key: 'key-value' },
@@ -741,16 +733,16 @@ RSpec.describe Integration do
end
it 'filters out sensitive fields' do
- expect(service.api_field_names).to eq(['safe_field'])
+ expect(integration.api_field_names).to eq(['safe_field'])
end
end
context 'logging' do
- let(:service) { build(:service, project: project) }
+ let(:integration) { build(:integration, project: project) }
let(:test_message) { "test message" }
let(:arguments) do
{
- service_class: service.class.name,
+ service_class: integration.class.name,
project_path: project.full_path,
project_id: project.id,
message: test_message,
@@ -761,20 +753,20 @@ RSpec.describe Integration do
it 'logs info messages using json logger' do
expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
- service.log_info(test_message, additional_argument: 'some argument')
+ integration.log_info(test_message, additional_argument: 'some argument')
end
it 'logs error messages using json logger' do
expect(Gitlab::JsonLogger).to receive(:error).with(arguments)
- service.log_error(test_message, additional_argument: 'some argument')
+ integration.log_error(test_message, additional_argument: 'some argument')
end
context 'when project is nil' do
let(:project) { nil }
let(:arguments) do
{
- service_class: service.class.name,
+ service_class: integration.class.name,
project_path: nil,
project_id: nil,
message: test_message,
@@ -785,7 +777,7 @@ RSpec.describe Integration do
it 'logs info messages using json logger' do
expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
- service.log_info(test_message, additional_argument: 'some argument')
+ integration.log_info(test_message, additional_argument: 'some argument')
end
end
end
diff --git a/spec/models/integrations/asana_spec.rb b/spec/models/integrations/asana_spec.rb
index f7e7eb1b0ae..b6602964182 100644
--- a/spec/models/integrations/asana_spec.rb
+++ b/spec/models/integrations/asana_spec.rb
@@ -14,27 +14,29 @@ RSpec.describe Integrations::Asana do
end
describe 'Execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
let(:gid) { "123456789ABCD" }
+ let(:asana_task) { double(::Asana::Resources::Task) }
+ let(:asana_integration) { described_class.new }
- def create_data_for_commits(*messages)
+ let(:data) do
{
object_kind: 'push',
ref: 'master',
user_name: user.name,
- commits: messages.map do |m|
+ commits: [
{
- message: m,
+ message: message,
url: 'https://gitlab.com/'
}
- end
+ ]
}
end
before do
- @asana = described_class.new
- allow(@asana).to receive_messages(
+ allow(asana_integration).to receive_messages(
project: project,
project_id: project.id,
api_key: 'verySecret',
@@ -42,67 +44,79 @@ RSpec.describe Integrations::Asana do
)
end
- it 'calls Asana integration to create a story' do
- data = create_data_for_commits("Message from commit. related to ##{gid}")
- expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
+ subject(:execute_integration) { asana_integration.execute(data) }
+
+ context 'when creating a story' do
+ let(:message) { "Message from commit. related to ##{gid}" }
+ let(:expected_message) do
+ "#{user.name} pushed to branch master of #{project.full_name} ( https://gitlab.com/ ): #{message}"
+ end
- d1 = double('Asana::Resources::Task')
- expect(d1).to receive(:add_comment).with(text: expected_message)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
+ it 'calls Asana integration to create a story' do
+ expect(asana_task).to receive(:add_comment).with(text: expected_message)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(asana_task)
- @asana.execute(data)
+ execute_integration
+ end
end
- it 'calls Asana integration to create a story and close a task' do
- data = create_data_for_commits('fix #456789')
- d1 = double('Asana::Resources::Task')
- expect(d1).to receive(:add_comment)
- expect(d1).to receive(:update).with(completed: true)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+ context 'when creating a story and closing a task' do
+ let(:message) { 'fix #456789' }
- @asana.execute(data)
+ it 'calls Asana integration to create a story and close a task' do
+ expect(asana_task).to receive(:add_comment)
+ expect(asana_task).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(asana_task)
+
+ execute_integration
+ end
end
- it 'is able to close via url' do
- data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
- d1 = double('Asana::Resources::Task')
- expect(d1).to receive(:add_comment)
- expect(d1).to receive(:update).with(completed: true)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+ context 'when closing via url' do
+ let(:message) { 'closes https://app.asana.com/19292/956299/42' }
- @asana.execute(data)
+ it 'calls Asana integration to close via url' do
+ expect(asana_task).to receive(:add_comment)
+ expect(asana_task).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(asana_task)
+
+ execute_integration
+ end
end
- it 'allows multiple matches per line' do
- message = <<-EOF
- minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
- ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
- EOF
- data = create_data_for_commits(message)
- d1 = double('Asana::Resources::Task')
- expect(d1).to receive(:add_comment)
- expect(d1).to receive(:update).with(completed: true)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
-
- d2 = double('Asana::Resources::Task')
- expect(d2).to receive(:add_comment)
- expect(d2).to receive(:update).with(completed: true)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
-
- d3 = double('Asana::Resources::Task')
- expect(d3).to receive(:add_comment)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
-
- d4 = double('Asana::Resources::Task')
- expect(d4).to receive(:add_comment)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
-
- d5 = double('Asana::Resources::Task')
- expect(d5).to receive(:add_comment)
- expect(d5).to receive(:update).with(completed: true)
- expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
-
- @asana.execute(data)
+ context 'with multiple matches per line' do
+ let(:message) do
+ <<-EOF
+ minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
+ ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
+ EOF
+ end
+
+ it 'allows multiple matches per line' do
+ expect(asana_task).to receive(:add_comment)
+ expect(asana_task).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(asana_task)
+
+ asana_task_2 = double(Asana::Resources::Task)
+ expect(asana_task_2).to receive(:add_comment)
+ expect(asana_task_2).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(asana_task_2)
+
+ asana_task_3 = double(Asana::Resources::Task)
+ expect(asana_task_3).to receive(:add_comment)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(asana_task_3)
+
+ asana_task_4 = double(Asana::Resources::Task)
+ expect(asana_task_4).to receive(:add_comment)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(asana_task_4)
+
+ asana_task_5 = double(Asana::Resources::Task)
+ expect(asana_task_5).to receive(:add_comment)
+ expect(asana_task_5).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(asana_task_5)
+
+ execute_integration
+ end
end
end
end
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index 9c3ff7aa35b..9856c53a390 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -38,6 +38,11 @@ RSpec.describe Integrations::Datadog do
let(:pipeline_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
let(:build_data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:archive_trace_data) do
+ create(:ci_job_artifact, :trace, job: build)
+
+ Gitlab::DataBuilder::ArchiveTrace.build(build)
+ end
it_behaves_like Integrations::HasWebHook do
let(:integration) { instance }
@@ -100,6 +105,13 @@ RSpec.describe Integrations::Datadog do
end
end
+ describe '#help' do
+ subject { instance.help }
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.not_to be_empty }
+ end
+
describe '#hook_url' do
subject { instance.hook_url }
@@ -161,13 +173,16 @@ RSpec.describe Integrations::Datadog do
end
before do
+ stub_feature_flags(datadog_integration_logs_collection: enable_logs_collection)
stub_request(:post, expected_hook_url)
saved_instance.execute(data)
end
+ let(:enable_logs_collection) { true }
+
context 'with pipeline data' do
let(:data) { pipeline_data }
- let(:expected_headers) { { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' } }
+ let(:expected_headers) { { ::Gitlab::WebHooks::GITLAB_EVENT_HEADER => 'Pipeline Hook' } }
let(:expected_body) { data.with_retried_builds.to_json }
it { expect(a_request(:post, expected_hook_url).with(headers: expected_headers, body: expected_body)).to have_been_made }
@@ -175,10 +190,24 @@ RSpec.describe Integrations::Datadog do
context 'with job data' do
let(:data) { build_data }
- let(:expected_headers) { { WebHookService::GITLAB_EVENT_HEADER => 'Job Hook' } }
+ let(:expected_headers) { { ::Gitlab::WebHooks::GITLAB_EVENT_HEADER => 'Job Hook' } }
+ let(:expected_body) { data.to_json }
+
+ it { expect(a_request(:post, expected_hook_url).with(headers: expected_headers, body: expected_body)).to have_been_made }
+ end
+
+ context 'with archive trace data' do
+ let(:data) { archive_trace_data }
+ let(:expected_headers) { { ::Gitlab::WebHooks::GITLAB_EVENT_HEADER => 'Archive Trace Hook' } }
let(:expected_body) { data.to_json }
it { expect(a_request(:post, expected_hook_url).with(headers: expected_headers, body: expected_body)).to have_been_made }
+
+ context 'but feature flag disabled' do
+ let(:enable_logs_collection) { false }
+
+ it { expect(a_request(:post, expected_hook_url)).not_to have_been_made }
+ end
end
end
end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index 9163a7ef845..e80fa6e3b70 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -937,18 +937,6 @@ RSpec.describe Integrations::Jira do
end
end
- context 'with jira_use_first_ref_by_oid feature flag disabled' do
- before do
- stub_feature_flags(jira_use_first_ref_by_oid: false)
- end
-
- it 'creates a comment and remote link on Jira' do
- expect(subject).to eq(success_message)
- expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
- expect(WebMock).to have_requested(:post, remote_link_url).once
- end
- end
-
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 51b27151ba2..f0007e1203c 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe InternalId do
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } # rubocop: disable Database/MultipleDatabases
+ allow(ApplicationRecord.connection).to receive(:transaction_open?) { false }
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original
@@ -146,7 +146,7 @@ RSpec.describe InternalId do
let(:value) { 2 }
it 'increments counter with in_transaction: "false"' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } # rubocop: disable Database/MultipleDatabases
+ allow(ApplicationRecord.connection).to receive(:transaction_open?) { false }
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original
@@ -217,7 +217,7 @@ RSpec.describe InternalId do
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } # rubocop: disable Database/MultipleDatabases
+ allow(ApplicationRecord.connection).to receive(:transaction_open?) { false }
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 4cbfa7c7758..c105f6c3439 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Issue do
it { is_expected.to belong_to(:iteration) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:namespace).through(:project) }
- it { is_expected.to belong_to(:work_item_type).class_name('WorkItem::Type') }
+ it { is_expected.to belong_to(:work_item_type).class_name('WorkItems::Type') }
it { is_expected.to belong_to(:moved_to).class_name('Issue') }
it { is_expected.to have_one(:moved_from).class_name('Issue') }
it { is_expected.to belong_to(:duplicated_to).class_name('Issue') }
@@ -238,6 +238,17 @@ RSpec.describe Issue do
end
end
+ # TODO: Remove when NOT NULL constraint is added to the relationship
+ describe '#work_item_type' do
+ let(:issue) { create(:issue, :incident, project: reusable_project, work_item_type: nil) }
+
+ it 'returns a default type if the legacy issue does not have a work item type associated yet' do
+ expect(issue.work_item_type_id).to be_nil
+ expect(issue.issue_type).to eq('incident')
+ expect(issue.work_item_type).to eq(WorkItems::Type.default_by_type(:incident))
+ end
+ end
+
describe '#sort' do
let(:project) { reusable_project }
@@ -1317,28 +1328,10 @@ RSpec.describe Issue do
let_it_be(:issue1) { create(:issue, project: project, relative_position: nil) }
let_it_be(:issue2) { create(:issue, project: project, relative_position: nil) }
- context 'when optimized_issue_neighbor_queries is enabled' do
- before do
- stub_feature_flags(optimized_issue_neighbor_queries: true)
- end
-
- it_behaves_like "a class that supports relative positioning" do
- let_it_be(:project) { reusable_project }
- let(:factory) { :issue }
- let(:default_params) { { project: project } }
- end
- end
-
- context 'when optimized_issue_neighbor_queries is disabled' do
- before do
- stub_feature_flags(optimized_issue_neighbor_queries: false)
- end
-
- it_behaves_like "a class that supports relative positioning" do
- let_it_be(:project) { reusable_project }
- let(:factory) { :issue }
- let(:default_params) { { project: project } }
- end
+ it_behaves_like "a class that supports relative positioning" do
+ let_it_be(:project) { reusable_project }
+ let(:factory) { :issue }
+ let(:default_params) { { project: project } }
end
it 'is not blocked for repositioning by default' do
@@ -1580,4 +1573,13 @@ RSpec.describe Issue do
expect(participant.issue.email_participants_emails_downcase).to match([participant.email.downcase])
end
end
+
+ describe '#escalation_status' do
+ it 'returns the incident_management_issuable_escalation_status association' do
+ escalation_status = create(:incident_management_issuable_escalation_status)
+ issue = escalation_status.issue
+
+ expect(issue.escalation_status).to eq(escalation_status)
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index d41a1604211..19459561edf 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -21,6 +21,28 @@ RSpec.describe Key, :mailer do
it { is_expected.to allow_value(attributes_for(:ecdsa_key_256)[:key]).for(:key) }
it { is_expected.to allow_value(attributes_for(:ed25519_key_256)[:key]).for(:key) }
it { is_expected.not_to allow_value('foo-bar').for(:key) }
+
+ context 'key format' do
+ let(:key) { build(:key) }
+
+ it 'does not allow the key that begins with an algorithm name that is unsupported' do
+ key.key = 'unsupported-ssh-rsa key'
+
+ key.valid?
+
+ expect(key.errors.of_kind?(:key, :invalid)).to eq(true)
+ end
+
+ Gitlab::SSHPublicKey.supported_algorithms.each do |supported_algorithm|
+ it "allows the key that begins with supported algorithm name '#{supported_algorithm}'" do
+ key.key = "#{supported_algorithm} key"
+
+ key.valid?
+
+ expect(key.errors.of_kind?(:key, :invalid)).to eq(false)
+ end
+ end
+ end
end
describe "Methods" do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 7ce32de6edc..1957c58ec81 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Member do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:member_namespace) }
it { is_expected.to have_one(:member_task) }
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index e1db1b3cf3e..4005a2ec6da 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1648,10 +1648,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
it 'uses template from target project' do
request = build(:merge_request, title: 'Fix everything')
- request.compare_commits = [
- double(safe_message: 'Commit message', gitaly_commit?: true, merge_commit?: false, description?: false)
- ]
- subject.target_project.merge_commit_template = '%{title}'
+ request.target_project.merge_commit_template = '%{title}'
expect(request.default_merge_commit_message)
.to eq('Fix everything')
@@ -3495,84 +3492,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
- describe "#environments_for" do
- let(:project) { create(:project, :repository) }
- let(:user) { project.creator }
- let(:merge_request) { create(:merge_request, source_project: project) }
- let(:source_branch) { merge_request.source_branch }
- let(:target_branch) { merge_request.target_branch }
- let(:source_oid) { project.commit(source_branch).id }
- let(:target_oid) { project.commit(target_branch).id }
-
- before do
- merge_request.source_project.add_maintainer(user)
- merge_request.target_project.add_maintainer(user)
- end
-
- context 'with multiple environments' do
- let(:environments) { create_list(:environment, 3, project: project) }
-
- before do
- create(:deployment, :success, environment: environments.first, ref: source_branch, sha: source_oid)
- create(:deployment, :success, environment: environments.second, ref: target_branch, sha: target_oid)
- end
-
- it 'selects deployed environments' do
- expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
- end
-
- it 'selects latest deployed environment' do
- latest_environment = create(:environment, project: project)
- create(:deployment, :success, environment: latest_environment, ref: source_branch, sha: source_oid)
-
- expect(merge_request.environments_for(user)).to eq([environments.first, latest_environment])
- expect(merge_request.environments_for(user, latest: true)).to contain_exactly(latest_environment)
- end
- end
-
- context 'with environments on source project' do
- let(:source_project) { fork_project(project, nil, repository: true) }
-
- let(:merge_request) do
- create(:merge_request,
- source_project: source_project, source_branch: 'feature',
- target_project: project)
- end
-
- let(:source_environment) { create(:environment, project: source_project) }
-
- before do
- create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
- end
-
- it 'selects deployed environments', :sidekiq_might_not_need_inline do
- expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
- end
-
- context 'with environments on target project' do
- let(:target_environment) { create(:environment, project: project) }
-
- before do
- create(:deployment, :success, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
- end
-
- it 'selects deployed environments', :sidekiq_might_not_need_inline do
- expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
- end
- end
- end
-
- context 'without a diff_head_commit' do
- before do
- expect(merge_request).to receive(:diff_head_commit).and_return(nil)
- end
-
- it 'returns an empty array' do
- expect(merge_request.environments_for(user)).to be_empty
- end
- end
- end
-
describe "#environments" do
subject { merge_request.environments }
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 429727c2360..c9f8a1bcdc2 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -126,57 +126,4 @@ RSpec.describe NamespaceSetting, type: :model do
end
end
end
-
- describe 'hooks related to group user cap update' do
- let(:settings) { create(:namespace_settings, new_user_signups_cap: user_cap) }
- let(:group) { create(:group, namespace_settings: settings) }
-
- before do
- allow(group).to receive(:root?).and_return(true)
- end
-
- context 'when updating a group with a user cap' do
- let(:user_cap) { nil }
-
- it 'also sets share_with_group_lock and prevent_sharing_groups_outside_hierarchy to true' do
- expect(group.new_user_signups_cap).to be_nil
- expect(group.share_with_group_lock).to be_falsey
- expect(settings.prevent_sharing_groups_outside_hierarchy).to be_falsey
-
- settings.update!(new_user_signups_cap: 10)
- group.reload
-
- expect(group.new_user_signups_cap).to eq(10)
- expect(group.share_with_group_lock).to be_truthy
- expect(settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
- end
-
- it 'has share_with_group_lock and prevent_sharing_groups_outside_hierarchy returning true for descendent groups' do
- descendent = create(:group, parent: group)
- desc_settings = descendent.namespace_settings
-
- expect(descendent.share_with_group_lock).to be_falsey
- expect(desc_settings.prevent_sharing_groups_outside_hierarchy).to be_falsey
-
- settings.update!(new_user_signups_cap: 10)
-
- expect(descendent.reload.share_with_group_lock).to be_truthy
- expect(desc_settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
- end
- end
-
- context 'when removing a user cap from namespace settings' do
- let(:user_cap) { 10 }
-
- it 'leaves share_with_group_lock and prevent_sharing_groups_outside_hierarchy set to true to the related group' do
- expect(group.share_with_group_lock).to be_truthy
- expect(settings.prevent_sharing_groups_outside_hierarchy).to be_truthy
-
- settings.update!(new_user_signups_cap: nil)
-
- expect(group.reload.share_with_group_lock).to be_truthy
- expect(settings.reload.prevent_sharing_groups_outside_hierarchy).to be_truthy
- end
- end
- end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 54327fc70d9..5da0f7a134c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -28,6 +28,8 @@ RSpec.describe Namespace do
it { is_expected.to have_one :onboarding_progress }
it { is_expected.to have_one :admin_note }
it { is_expected.to have_many :pending_builds }
+ it { is_expected.to have_one :namespace_route }
+ it { is_expected.to have_many :namespace_members }
describe '#children' do
let_it_be(:group) { create(:group) }
@@ -1263,6 +1265,32 @@ RSpec.describe Namespace do
end
end
+ describe '#use_traversal_ids_for_self_and_hierarchy?' do
+ let_it_be(:namespace, reload: true) { create(:namespace) }
+
+ subject { namespace.use_traversal_ids_for_self_and_hierarchy? }
+
+ it { is_expected.to eq true }
+
+ it_behaves_like 'disabled feature flag when traversal_ids is blank'
+
+ context 'when use_traversal_ids_for_self_and_hierarchy feature flag is false' do
+ before do
+ stub_feature_flags(use_traversal_ids_for_self_and_hierarchy: false)
+ end
+
+ it { is_expected.to eq false }
+ end
+
+ context 'when use_traversal_ids? feature flag is false' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
+
+ it { is_expected.to eq false }
+ end
+ end
+
describe '#users_with_descendants' do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
diff --git a/spec/models/namespaces/project_namespace_spec.rb b/spec/models/namespaces/project_namespace_spec.rb
index 4416c49f1bf..47cf866c143 100644
--- a/spec/models/namespaces/project_namespace_spec.rb
+++ b/spec/models/namespaces/project_namespace_spec.rb
@@ -17,11 +17,11 @@ RSpec.describe Namespaces::ProjectNamespace, type: :model do
let_it_be(:project) { create(:project) }
let_it_be(:project_namespace) { project.project_namespace }
- it 'also deletes the associated project' do
+ it 'keeps the associated project' do
project_namespace.delete
expect { project_namespace.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { project.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(project.reload.project_namespace).to be_nil
end
end
end
diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb
index deac8d29196..80a39404d10 100644
--- a/spec/models/onboarding_progress_spec.rb
+++ b/spec/models/onboarding_progress_spec.rb
@@ -131,29 +131,86 @@ RSpec.describe OnboardingProgress do
end
describe '.register' do
- subject(:register_action) { described_class.register(namespace, action) }
+ context 'for a single action' do
+ subject(:register_action) { described_class.register(namespace, action) }
- context 'when the namespace was onboarded' do
- before do
- described_class.onboard(namespace)
- end
+ context 'when the namespace was onboarded' do
+ before do
+ described_class.onboard(namespace)
+ end
- it 'registers the action for the namespace' do
- expect { register_action }.to change { described_class.completed?(namespace, action) }.from(false).to(true)
- end
+ it 'registers the action for the namespace' do
+ expect { register_action }.to change { described_class.completed?(namespace, action) }.from(false).to(true)
+ end
- context 'when the action does not exist' do
- let(:action) { :foo }
+ it 'does not override timestamp', :aggregate_failures do
+ expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).to be_nil
+ register_action
+ expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).not_to be_nil
+ expect { described_class.register(namespace, action) }.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at }
+ end
+
+ context 'when the action does not exist' do
+ let(:action) { :foo }
+ it 'does not register the action for the namespace' do
+ expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(nil)
+ end
+ end
+ end
+
+ context 'when the namespace was not onboarded' do
it 'does not register the action for the namespace' do
- expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(nil)
+ expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(false)
end
end
end
- context 'when the namespace was not onboarded' do
- it 'does not register the action for the namespace' do
- expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(false)
+ context 'for multiple actions' do
+ let(:action1) { :security_scan_enabled }
+ let(:action2) { :secure_dependency_scanning_run }
+ let(:actions) { [action1, action2] }
+
+ subject(:register_action) { described_class.register(namespace, actions) }
+
+ context 'when the namespace was onboarded' do
+ before do
+ described_class.onboard(namespace)
+ end
+
+ it 'registers the actions for the namespace' do
+ expect { register_action }.to change {
+ [described_class.completed?(namespace, action1), described_class.completed?(namespace, action2)]
+ }.from([false, false]).to([true, true])
+ end
+
+ it 'does not override timestamp', :aggregate_failures do
+ described_class.register(namespace, [action1])
+ expect(described_class.find_by_namespace_id(namespace.id).security_scan_enabled_at).not_to be_nil
+ expect(described_class.find_by_namespace_id(namespace.id).secure_dependency_scanning_run_at).to be_nil
+
+ expect { described_class.register(namespace, [action1, action2]) }.not_to change {
+ described_class.find_by_namespace_id(namespace.id).security_scan_enabled_at
+ }
+ expect(described_class.find_by_namespace_id(namespace.id).secure_dependency_scanning_run_at).not_to be_nil
+ end
+
+ context 'when one of the actions does not exist' do
+ let(:action2) { :foo }
+
+ it 'does not register any action for the namespace' do
+ expect { register_action }.not_to change {
+ [described_class.completed?(namespace, action1), described_class.completed?(namespace, action2)]
+ }.from([false, nil])
+ end
+ end
+ end
+
+ context 'when the namespace was not onboarded' do
+ it 'does not register the action for the namespace' do
+ expect { register_action }.not_to change { described_class.completed?(namespace, action1) }.from(false)
+ expect { described_class.register(namespace, action) }.not_to change { described_class.completed?(namespace, action2) }.from(false)
+ end
end
end
end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 8617793f41d..a86caa074f1 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -10,6 +10,9 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:package_file3) { create(:package_file, :xml, file_name: 'formatted.zip') }
let_it_be(:debian_package) { create(:debian_package, project: project) }
+ it_behaves_like 'having unique enum values'
+ it_behaves_like 'destructible', factory: :package_file
+
describe 'relationships' do
it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
@@ -138,6 +141,24 @@ RSpec.describe Packages::PackageFile, type: :model do
it 'returns the matching file only for Helm packages' do
expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2)
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:helm_package_file, :pending_destruction, package: helm_package2, channel: channel) }
+
+ it 'does not return them' do
+ expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2, package_file_pending_destruction)
+ end
+ end
+ end
end
describe '.most_recent!' do
@@ -154,15 +175,17 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:package_file3_2) { create(:package_file, :npm, package: package3) }
let_it_be(:package_file3_3) { create(:package_file, :npm, package: package3) }
+ let_it_be(:package_file3_4) { create(:package_file, :npm, :pending_destruction, package: package3) }
let_it_be(:package_file4_2) { create(:package_file, :npm, package: package2) }
let_it_be(:package_file4_3) { create(:package_file, :npm, package: package2) }
let_it_be(:package_file4_4) { create(:package_file, :npm, package: package2) }
+ let_it_be(:package_file4_4) { create(:package_file, :npm, :pending_destruction, package: package2) }
- let(:most_recent_package_file1) { package1.package_files.recent.first }
- let(:most_recent_package_file2) { package2.package_files.recent.first }
- let(:most_recent_package_file3) { package3.package_files.recent.first }
- let(:most_recent_package_file4) { package4.package_files.recent.first }
+ let(:most_recent_package_file1) { package1.installable_package_files.recent.first }
+ let(:most_recent_package_file2) { package2.installable_package_files.recent.first }
+ let(:most_recent_package_file3) { package3.installable_package_files.recent.first }
+ let(:most_recent_package_file4) { package4.installable_package_files.recent.first }
subject { described_class.most_recent_for(packages) }
@@ -202,6 +225,24 @@ RSpec.describe Packages::PackageFile, type: :model do
it 'returns the most recent package for the selected channel' do
expect(subject).to contain_exactly(helm_package_file2)
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:helm_package_file, :pending_destruction, package: helm_package, channel: 'alpha') }
+
+ it 'does not return them' do
+ expect(subject).to contain_exactly(helm_package_file2)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(subject).to contain_exactly(package_file_pending_destruction)
+ end
+ end
+ end
end
end
@@ -314,4 +355,25 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
end
+
+ context 'status scopes' do
+ let_it_be(:package) { create(:package) }
+ let_it_be(:default_package_file) { create(:package_file, package: package) }
+ let_it_be(:pending_destruction_package_file) { create(:package_file, :pending_destruction, package: package) }
+
+ describe '.installable' do
+ subject { package.installable_package_files }
+
+ it 'does not include non-displayable packages', :aggregate_failures do
+ is_expected.to include(default_package_file)
+ is_expected.not_to include(pending_destruction_package_file)
+ end
+ end
+
+ describe '.with_status' do
+ subject { described_class.with_status(:pending_destruction) }
+
+ it { is_expected.to contain_exactly(pending_destruction_package_file) }
+ end
+ end
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 44ba6e0e2fd..122340f7bec 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -413,9 +413,17 @@ RSpec.describe Packages::Package, type: :model do
it_behaves_like 'validating version to be SemVer compliant for', :terraform_module_package
context 'nuget package' do
- it_behaves_like 'validating version to be SemVer compliant for', :nuget_package
+ subject { build_stubbed(:nuget_package) }
+ it { is_expected.to allow_value('1.2').for(:version) }
+ it { is_expected.to allow_value('1.2.3').for(:version) }
it { is_expected.to allow_value('1.2.3.4').for(:version) }
+ it { is_expected.to allow_value('1.2.3-beta').for(:version) }
+ it { is_expected.to allow_value('1.2.3-alpha.3').for(:version) }
+ it { is_expected.not_to allow_value('1').for(:version) }
+ it { is_expected.not_to allow_value('1./2.3').for(:version) }
+ it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
end
end
@@ -839,6 +847,7 @@ RSpec.describe Packages::Package, type: :model do
end
context 'status scopes' do
+ let_it_be(:default_package) { create(:maven_package, :default) }
let_it_be(:hidden_package) { create(:maven_package, :hidden) }
let_it_be(:processing_package) { create(:maven_package, :processing) }
let_it_be(:error_package) { create(:maven_package, :error) }
@@ -856,11 +865,15 @@ RSpec.describe Packages::Package, type: :model do
describe '.installable' do
subject { described_class.installable }
- it 'does not include non-displayable packages', :aggregate_failures do
+ it 'does not include non-installable packages', :aggregate_failures do
is_expected.not_to include(error_package)
- is_expected.not_to include(hidden_package)
is_expected.not_to include(processing_package)
end
+
+ it 'includes installable packages', :aggregate_failures do
+ is_expected.to include(default_package)
+ is_expected.to include(hidden_package)
+ end
end
describe '.with_status' do
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 8a5b1e73194..0735bf25690 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -336,129 +336,6 @@ RSpec.describe PagesDomain do
end
end
- describe '#update_daemon' do
- let_it_be(:project) { create(:project).tap(&:mark_pages_as_deployed) }
-
- context 'when usage is serverless' do
- it 'does not call the UpdatePagesConfigurationService' do
- expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async)
-
- create(:pages_domain, usage: :serverless)
- end
- end
-
- it 'runs when the domain is created' do
- domain = build(:pages_domain)
-
- expect(domain).to receive(:update_daemon)
-
- domain.save!
- end
-
- it 'runs when the domain is destroyed' do
- domain = create(:pages_domain)
-
- expect(domain).to receive(:update_daemon)
-
- domain.destroy!
- end
-
- it "schedules a PagesUpdateConfigurationWorker" do
- expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id)
-
- create(:pages_domain, project: project)
- end
-
- context "when the pages aren't deployed" do
- let_it_be(:project) { create(:project).tap(&:mark_pages_as_not_deployed) }
-
- it "does not schedule a PagesUpdateConfigurationWorker" do
- expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async).with(project.id)
-
- create(:pages_domain, project: project)
- end
- end
-
- context 'configuration updates when attributes change' do
- let_it_be(:project1) { create(:project) }
- let_it_be(:project2) { create(:project) }
- let_it_be(:domain) { create(:pages_domain) }
-
- where(:attribute, :old_value, :new_value, :update_expected) do
- now = Time.current
- future = now + 1.day
-
- :project | nil | :project1 | true
- :project | :project1 | :project1 | false
- :project | :project1 | :project2 | true
- :project | :project1 | nil | true
-
- # domain can't be set to nil
- :domain | 'a.com' | 'a.com' | false
- :domain | 'a.com' | 'b.com' | true
-
- # verification_code can't be set to nil
- :verification_code | 'foo' | 'foo' | false
- :verification_code | 'foo' | 'bar' | false
-
- :verified_at | nil | now | false
- :verified_at | now | now | false
- :verified_at | now | future | false
- :verified_at | now | nil | false
-
- :enabled_until | nil | now | true
- :enabled_until | now | now | false
- :enabled_until | now | future | false
- :enabled_until | now | nil | true
- end
-
- with_them do
- it 'runs if a relevant attribute has changed' do
- a = old_value.is_a?(Symbol) ? send(old_value) : old_value
- b = new_value.is_a?(Symbol) ? send(new_value) : new_value
-
- domain.update!(attribute => a)
-
- if update_expected
- expect(domain).to receive(:update_daemon)
- else
- expect(domain).not_to receive(:update_daemon)
- end
-
- domain.update!(attribute => b)
- end
- end
-
- context 'TLS configuration' do
- let_it_be(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) }
- let_it_be(:domain) { create(:pages_domain) }
-
- let(:cert1) { domain.certificate }
- let(:cert2) { cert1 + ' ' }
- let(:key1) { domain.key }
- let(:key2) { key1 + ' ' }
-
- it 'updates when added' do
- expect(domain_without_tls).to receive(:update_daemon)
-
- domain_without_tls.update!(key: key1, certificate: cert1)
- end
-
- it 'updates when changed' do
- expect(domain).to receive(:update_daemon)
-
- domain.update!(key: key2, certificate: cert2)
- end
-
- it 'updates when removed' do
- expect(domain).to receive(:update_daemon)
-
- domain.update!(key: nil, certificate: nil)
- end
- end
- end
- end
-
describe '#user_provided_key' do
subject { domain.user_provided_key }
diff --git a/spec/models/preloaders/environments/deployment_preloader_spec.rb b/spec/models/preloaders/environments/deployment_preloader_spec.rb
new file mode 100644
index 00000000000..c1812d45628
--- /dev/null
+++ b/spec/models/preloaders/environments/deployment_preloader_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::Environments::DeploymentPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :repository) }
+
+ let_it_be(:pipeline) { create(:ci_pipeline, user: user, project: project, sha: project.commit.sha) }
+ let_it_be(:ci_build_a) { create(:ci_build, user: user, project: project, pipeline: pipeline) }
+ let_it_be(:ci_build_b) { create(:ci_build, user: user, project: project, pipeline: pipeline) }
+ let_it_be(:ci_build_c) { create(:ci_build, user: user, project: project, pipeline: pipeline) }
+
+ let_it_be(:environment_a) { create(:environment, project: project, state: :available) }
+ let_it_be(:environment_b) { create(:environment, project: project, state: :available) }
+
+ before do
+ create(:deployment, :success, project: project, environment: environment_a, deployable: ci_build_a)
+ create(:deployment, :success, project: project, environment: environment_a, deployable: ci_build_b)
+ create(:deployment, :success, project: project, environment: environment_b, deployable: ci_build_c)
+ end
+
+ def preload_association(association_name)
+ described_class.new(project.environments)
+ .execute_with_union(association_name, deployment_associations)
+ end
+
+ def deployment_associations
+ {
+ user: [],
+ deployable: {
+ pipeline: {
+ manual_actions: []
+ }
+ }
+ }
+ end
+
+ it 'does not trigger N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { preload_association(:last_deployment) }
+
+ ci_build_d = create(:ci_build, user: user, project: project, pipeline: pipeline)
+ create(:deployment, :success, project: project, environment: environment_b, deployable: ci_build_d)
+
+ expect { preload_association(:last_deployment) }.not_to exceed_query_limit(control)
+ end
+
+ it 'batch loads the dependent associations' do
+ preload_association(:last_deployment)
+
+ expect do
+ project.environments.first.last_deployment.deployable.pipeline.manual_actions
+ end.not_to exceed_query_limit(0)
+ end
+
+ # Example query scoped with IN clause for `last_deployment` association preload:
+ # SELECT DISTINCT ON (environment_id) deployments.* FROM "deployments" WHERE "deployments"."status" IN (1, 2, 3, 4, 6) AND "deployments"."environment_id" IN (35, 34, 33) ORDER BY environment_id, deployments.id DESC
+ it 'avoids scoping with IN clause during preload' do
+ control = ActiveRecord::QueryRecorder.new { preload_association(:last_deployment) }
+
+ default_preload_query = control.occurrences_by_line_method.first[1][:occurrences].any? { |i| i.include?('"deployments"."environment_id" IN') }
+
+ expect(default_preload_query).to be(false)
+ end
+end
diff --git a/spec/models/project_pages_metadatum_spec.rb b/spec/models/project_pages_metadatum_spec.rb
index 31a533e0363..af2f9b94871 100644
--- a/spec/models/project_pages_metadatum_spec.rb
+++ b/spec/models/project_pages_metadatum_spec.rb
@@ -18,4 +18,15 @@ RSpec.describe ProjectPagesMetadatum do
expect(described_class.only_on_legacy_storage).to eq([legacy_storage_project.pages_metadatum])
end
end
+
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:model) do
+ artifacts_archive = create(:ci_job_artifact, :legacy_archive)
+ metadatum = artifacts_archive.project.pages_metadatum
+ metadatum.artifacts_archive = artifacts_archive
+ metadatum
+ end
+
+ let!(:parent) { model.artifacts_archive }
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4e38bf7d3e3..2fe50f8c48a 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Project, factory_default: :keep do
include GitHelpers
include ExternalAuthorizationServiceHelpers
include ReloadHelpers
+ include StubGitlabCalls
using RSpec::Parameterized::TableSyntax
let_it_be(:namespace) { create_default(:namespace).freeze }
@@ -379,6 +380,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ it { is_expected.not_to allow_value('colon:in:path').for(:path) } # This is to validate that a specially crafted name cannot bypass a pattern match. See !72555
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_length_of(:path).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(2000) }
@@ -1298,7 +1300,7 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#default_owner' do
+ describe '#first_owner' do
let_it_be(:owner) { create(:user) }
let_it_be(:namespace) { create(:namespace, owner: owner) }
@@ -1306,7 +1308,7 @@ RSpec.describe Project, factory_default: :keep do
let(:project) { build(:project, namespace: namespace) }
it 'is the namespace owner' do
- expect(project.default_owner).to eq(owner)
+ expect(project.first_owner).to eq(owner)
end
end
@@ -1315,9 +1317,9 @@ RSpec.describe Project, factory_default: :keep do
let(:project) { build(:project, group: group, namespace: namespace) }
it 'is the group owner' do
- allow(group).to receive(:default_owner).and_return(Object.new)
+ allow(group).to receive(:first_owner).and_return(Object.new)
- expect(project.default_owner).to eq(group.default_owner)
+ expect(project.first_owner).to eq(group.first_owner)
end
end
end
@@ -1358,51 +1360,51 @@ RSpec.describe Project, factory_default: :keep do
project.reload.has_external_issue_tracker
end
- it 'is false when external issue tracker service is not active' do
- create(:service, project: project, category: 'issue_tracker', active: false)
+ it 'is false when external issue tracker integration is not active' do
+ create(:integration, project: project, category: 'issue_tracker', active: false)
is_expected.to eq(false)
end
- it 'is false when other service is active' do
- create(:service, project: project, category: 'not_issue_tracker', active: true)
+ it 'is false when other integration is active' do
+ create(:integration, project: project, category: 'not_issue_tracker', active: true)
is_expected.to eq(false)
end
- context 'when there is an active external issue tracker service' do
- let!(:service) do
- create(:service, project: project, type: 'JiraService', category: 'issue_tracker', active: true)
+ context 'when there is an active external issue tracker integration' do
+ let!(:integration) do
+ create(:integration, project: project, type: 'JiraService', category: 'issue_tracker', active: true)
end
specify { is_expected.to eq(true) }
- it 'becomes false when external issue tracker service is destroyed' do
+ it 'becomes false when external issue tracker integration is destroyed' do
expect do
- Integration.find(service.id).delete
+ Integration.find(integration.id).delete
end.to change { subject }.to(false)
end
- it 'becomes false when external issue tracker service becomes inactive' do
+ it 'becomes false when external issue tracker integration becomes inactive' do
expect do
- service.update_column(:active, false)
+ integration.update_column(:active, false)
end.to change { subject }.to(false)
end
- context 'when there are two active external issue tracker services' do
- let_it_be(:second_service) do
- create(:service, project: project, type: 'CustomIssueTracker', category: 'issue_tracker', active: true)
+ context 'when there are two active external issue tracker integrations' do
+ let_it_be(:second_integration) do
+ create(:integration, project: project, type: 'CustomIssueTracker', category: 'issue_tracker', active: true)
end
- it 'does not become false when external issue tracker service is destroyed' do
+ it 'does not become false when external issue tracker integration is destroyed' do
expect do
- Integration.find(service.id).delete
+ Integration.find(integration.id).delete
end.not_to change { subject }
end
- it 'does not become false when external issue tracker service becomes inactive' do
+ it 'does not become false when external issue tracker integration becomes inactive' do
expect do
- service.update_column(:active, false)
+ integration.update_column(:active, false)
end.not_to change { subject }
end
end
@@ -1454,13 +1456,13 @@ RSpec.describe Project, factory_default: :keep do
specify { expect(has_external_wiki).to eq(true) }
- it 'becomes false if the external wiki service is destroyed' do
+ it 'becomes false if the external wiki integration is destroyed' do
expect do
Integration.find(integration.id).delete
end.to change { has_external_wiki }.to(false)
end
- it 'becomes false if the external wiki service becomes inactive' do
+ it 'becomes false if the external wiki integration becomes inactive' do
expect do
integration.update_column(:active, false)
end.to change { has_external_wiki }.to(false)
@@ -4580,11 +4582,25 @@ RSpec.describe Project, factory_default: :keep do
include ProjectHelpers
let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, namespace: group) }
- let!(:project) { create(:project, project_level, namespace: group ) }
let(:user) { create_user_from_membership(project, membership) }
- context 'reporter level access' do
+ subject { described_class.filter_by_feature_visibility(feature, user) }
+
+ shared_examples 'filter respects visibility' do
+ it 'respects visibility' do
+ enable_admin_mode!(user) if admin_mode
+ project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_level.to_s))
+ update_feature_access_level(project, feature_access_level)
+
+ expected_objects = expected_count == 1 ? [project] : []
+
+ expect(subject).to eq(expected_objects)
+ end
+ end
+
+ context 'with reporter level access' do
let(:feature) { MergeRequest }
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
@@ -4592,20 +4608,11 @@ RSpec.describe Project, factory_default: :keep do
end
with_them do
- it "respects visibility" do
- enable_admin_mode!(user) if admin_mode
- update_feature_access_level(project, feature_access_level)
-
- expected_objects = expected_count == 1 ? [project] : []
-
- expect(
- described_class.filter_by_feature_visibility(feature, user)
- ).to eq(expected_objects)
- end
+ it_behaves_like 'filter respects visibility'
end
end
- context 'issues' do
+ context 'with feature issues' do
let(:feature) { Issue }
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
@@ -4613,20 +4620,11 @@ RSpec.describe Project, factory_default: :keep do
end
with_them do
- it "respects visibility" do
- enable_admin_mode!(user) if admin_mode
- update_feature_access_level(project, feature_access_level)
-
- expected_objects = expected_count == 1 ? [project] : []
-
- expect(
- described_class.filter_by_feature_visibility(feature, user)
- ).to eq(expected_objects)
- end
+ it_behaves_like 'filter respects visibility'
end
end
- context 'wiki' do
+ context 'with feature wiki' do
let(:feature) { :wiki }
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
@@ -4634,20 +4632,11 @@ RSpec.describe Project, factory_default: :keep do
end
with_them do
- it "respects visibility" do
- enable_admin_mode!(user) if admin_mode
- update_feature_access_level(project, feature_access_level)
-
- expected_objects = expected_count == 1 ? [project] : []
-
- expect(
- described_class.filter_by_feature_visibility(feature, user)
- ).to eq(expected_objects)
- end
+ it_behaves_like 'filter respects visibility'
end
end
- context 'code' do
+ context 'with feature code' do
let(:feature) { :repository }
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
@@ -4655,16 +4644,7 @@ RSpec.describe Project, factory_default: :keep do
end
with_them do
- it "respects visibility" do
- enable_admin_mode!(user) if admin_mode
- update_feature_access_level(project, feature_access_level)
-
- expected_objects = expected_count == 1 ? [project] : []
-
- expect(
- described_class.filter_by_feature_visibility(feature, user)
- ).to eq(expected_objects)
- end
+ it_behaves_like 'filter respects visibility'
end
end
end
@@ -6835,7 +6815,7 @@ RSpec.describe Project, factory_default: :keep do
describe 'with integrations and chat names' do
subject { create(:project) }
- let(:integration) { create(:service, project: subject) }
+ let(:integration) { create(:integration, project: subject) }
before do
create_list(:chat_name, 5, integration: integration)
@@ -7476,6 +7456,258 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#enforced_runner_token_expiration_interval and #effective_runner_token_expiration_interval' do
+ shared_examples 'no enforced expiration interval' do
+ it { expect(subject.enforced_runner_token_expiration_interval).to be_nil }
+ end
+
+ shared_examples 'enforced expiration interval' do |enforced_interval:|
+ it { expect(subject.enforced_runner_token_expiration_interval).to eq(enforced_interval) }
+ end
+
+ shared_examples 'no effective expiration interval' do
+ it { expect(subject.effective_runner_token_expiration_interval).to be_nil }
+ end
+
+ shared_examples 'effective expiration interval' do |effective_interval:|
+ it { expect(subject.effective_runner_token_expiration_interval).to eq(effective_interval) }
+ end
+
+ context 'when there is no interval' do
+ let_it_be(:project) { create(:project) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is a project interval' do
+ let_it_be(:project) { create(:project, runner_token_expiration_interval: 3.days.to_i) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ # runner_token_expiration_interval should not affect the expiration interval, only
+ # project_runner_token_expiration_interval should.
+ context 'when there is a site-wide enforced shared interval' do
+ before do
+ stub_application_setting(runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:project) { create(:project) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ # group_runner_token_expiration_interval should not affect the expiration interval, only
+ # project_runner_token_expiration_interval should.
+ context 'when there is a site-wide enforced group interval' do
+ before do
+ stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:project) { create(:project) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is a site-wide enforced project interval' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:project) { create(:project) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
+ it_behaves_like 'effective expiration interval', effective_interval: 5.days
+ end
+
+ # runner_token_expiration_interval should not affect the expiration interval, only
+ # project_runner_token_expiration_interval should.
+ context 'when there is a group-enforced group interval' do
+ let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ # subgroup_runner_token_expiration_interval should not affect the expiration interval, only
+ # project_runner_token_expiration_interval should.
+ context 'when there is a group-enforced subgroup interval' do
+ let_it_be(:group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ context 'when there is an owner group-enforced project interval' do
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ context 'when there is a grandparent group-enforced interval' do
+ let_it_be(:grandparent_group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 3.days.to_i) }
+ let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
+ let_it_be(:parent_group_settings) { create(:namespace_settings) }
+ let_it_be(:parent_group) { create(:group, parent: grandparent_group, namespace_settings: parent_group_settings) }
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, parent: parent_group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ context 'when there is a parent group-enforced interval overridden by group-enforced interval' do
+ let_it_be(:parent_group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 5.days.to_i) }
+ let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, parent: parent_group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ context 'when site-wide enforced interval overrides project interval' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 3.days.to_i)
+ end
+
+ let_it_be(:project) { create(:project, runner_token_expiration_interval: 4.days.to_i) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ context 'when project interval overrides site-wide enforced interval' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:project) { create(:project, runner_token_expiration_interval: 4.days.to_i) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+
+ it 'has human-readable expiration intervals' do
+ expect(subject.enforced_runner_token_expiration_interval_human_readable).to eq('5d')
+ expect(subject.effective_runner_token_expiration_interval_human_readable).to eq('4d')
+ end
+ end
+
+ context 'when site-wide enforced interval overrides group-enforced interval' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 3.days.to_i)
+ end
+
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ context 'when group-enforced interval overrides site-wide enforced interval' do
+ before do
+ stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
+ end
+
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ context 'when group-enforced interval overrides project interval' do
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 3.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group, runner_token_expiration_interval: 4.days.to_i) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
+ it_behaves_like 'effective expiration interval', effective_interval: 3.days
+ end
+
+ context 'when project interval overrides group-enforced interval' do
+ let_it_be(:group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 5.days.to_i) }
+ let_it_be(:group) { create(:group, namespace_settings: group_settings) }
+ let_it_be(:project) { create(:project, group: group, runner_token_expiration_interval: 4.days.to_i) }
+
+ subject { project }
+
+ it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
+ it_behaves_like 'effective expiration interval', effective_interval: 4.days
+ end
+
+ # Unrelated groups should not affect the expiration interval.
+ context 'when there is an enforced project interval in an unrelated group' do
+ let_it_be(:unrelated_group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:unrelated_group) { create(:group, namespace_settings: unrelated_group_settings) }
+ let_it_be(:project) { create(:project) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+
+ # Subgroups should not affect the parent group expiration interval.
+ context 'when there is an enforced project interval in a subgroup' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
+ let_it_be(:subgroup) { create(:group, parent: group, namespace_settings: subgroup_settings) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject { project }
+
+ it_behaves_like 'no enforced expiration interval'
+ it_behaves_like 'no effective expiration interval'
+ end
+ end
+
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :project }
end
@@ -7551,6 +7783,46 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#context_commits_enabled?' do
+ let_it_be(:project) { create(:project) }
+
+ subject(:result) { project.context_commits_enabled? }
+
+ context 'when context_commits feature flag is enabled' do
+ before do
+ stub_feature_flags(context_commits: true)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when context_commits feature flag is disabled' do
+ before do
+ stub_feature_flags(context_commits: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when context_commits feature flag is enabled on this project' do
+ before do
+ stub_feature_flags(context_commits: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when context_commits feature flag is enabled on another project' do
+ let(:another_project) { create(:project) }
+
+ before do
+ stub_feature_flags(context_commits: another_project)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
index ab3f455fe63..a256f9e0ab1 100644
--- a/spec/models/protectable_dropdown_spec.rb
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe ProtectableDropdown do
+ subject(:dropdown) { described_class.new(project, ref_type) }
+
let(:project) { create(:project, :repository) }
- let(:subject) { described_class.new(project, :branches) }
describe 'initialize' do
it 'raises ArgumentError for invalid ref type' do
@@ -13,34 +14,75 @@ RSpec.describe ProtectableDropdown do
end
end
- describe '#protectable_ref_names' do
+ shared_examples 'protectable_ref_names' do
context 'when project repository is not empty' do
- before do
- create(:protected_branch, project: project, name: 'master')
- end
-
- it { expect(subject.protectable_ref_names).to include('feature') }
- it { expect(subject.protectable_ref_names).not_to include('master') }
+ it 'includes elements matching a protected ref wildcard' do
+ is_expected.to include(matching_ref)
- it "includes branches matching a protected branch wildcard" do
- expect(subject.protectable_ref_names).to include('feature')
+ factory = ref_type == :branches ? :protected_branch : :protected_tag
- create(:protected_branch, name: 'feat*', project: project)
+ create(factory, name: "#{matching_ref[0]}*", project: project)
- subject = described_class.new(project.reload, :branches)
+ subject = described_class.new(project.reload, ref_type)
- expect(subject.protectable_ref_names).to include('feature')
+ expect(subject.protectable_ref_names).to include(matching_ref)
end
end
context 'when project repository is empty' do
let(:project) { create(:project) }
- it "returns empty list" do
- subject = described_class.new(project, :branches)
+ it 'returns empty list' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#protectable_ref_names' do
+ subject { dropdown.protectable_ref_names }
+
+ context 'for branches' do
+ let(:ref_type) { :branches }
+ let(:matching_ref) { 'feature' }
- expect(subject.protectable_ref_names).to be_empty
+ before do
+ create(:protected_branch, project: project, name: 'master')
end
+
+ it { is_expected.to include(matching_ref) }
+ it { is_expected.not_to include('master') }
+
+ it_behaves_like 'protectable_ref_names'
+ end
+
+ context 'for tags' do
+ let(:ref_type) { :tags }
+ let(:matching_ref) { 'v1.0.0' }
+
+ before do
+ create(:protected_tag, project: project, name: 'v1.1.0')
+ end
+
+ it { is_expected.to include(matching_ref) }
+ it { is_expected.not_to include('v1.1.0') }
+
+ it_behaves_like 'protectable_ref_names'
+ end
+ end
+
+ describe '#hash' do
+ subject { dropdown.hash }
+
+ context 'for branches' do
+ let(:ref_type) { :branches }
+
+ it { is_expected.to include(id: 'feature', text: 'feature', title: 'feature') }
+ end
+
+ context 'for tags' do
+ let(:ref_type) { :tags }
+
+ it { is_expected.to include(id: 'v1.0.0', text: 'v1.0.0', title: 'v1.0.0') }
end
end
end
diff --git a/spec/models/ref_matcher_spec.rb b/spec/models/ref_matcher_spec.rb
new file mode 100644
index 00000000000..47a6a8b986c
--- /dev/null
+++ b/spec/models/ref_matcher_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RefMatcher do
+ subject(:ref_matcher) { described_class.new(ref_pattern) }
+
+ let(:ref_pattern) { 'v1.0' }
+
+ shared_examples 'matching_refs' do
+ context 'when there is no match' do
+ let(:ref_pattern) { 'unknown' }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when ref pattern is a wildcard' do
+ let(:ref_pattern) { 'v*' }
+
+ it { is_expected.to match_array(refs) }
+ end
+ end
+
+ describe '#matching' do
+ subject { ref_matcher.matching(refs) }
+
+ context 'when refs are strings' do
+ let(:refs) { ['v1.0', 'v1.1'] }
+
+ it { is_expected.to match_array([ref_pattern]) }
+
+ it_behaves_like 'matching_refs'
+ end
+
+ context 'when refs are ref objects' do
+ let(:matching_ref) { double('tag', name: 'v1.0') }
+ let(:not_matching_ref) { double('tag', name: 'v1.1') }
+ let(:refs) { [matching_ref, not_matching_ref] }
+
+ it { is_expected.to match_array([matching_ref]) }
+
+ it_behaves_like 'matching_refs'
+ end
+ end
+
+ describe '#matches?' do
+ subject { ref_matcher.matches?(ref_name) }
+
+ let(:ref_name) { 'v1.0' }
+
+ it { is_expected.to be_truthy }
+
+ context 'when ref_name is empty' do
+ let(:ref_name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when ref pattern matches wildcard' do
+ let(:ref_pattern) { 'v*' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when ref pattern does not match wildcard' do
+ let(:ref_pattern) { 'v2.*' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#wildcard?' do
+ subject { ref_matcher.wildcard? }
+
+ it { is_expected.to be_falsey }
+
+ context 'when pattern is a wildcard' do
+ let(:ref_pattern) { 'v*' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 96cbdb468aa..e592a4964f5 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2398,17 +2398,6 @@ RSpec.describe Repository do
it 'returns nil when tag does not exists' do
expect(repository.find_tag('does-not-exist')).to be_nil
end
-
- context 'when find_tag_via_gitaly is disabled' do
- it 'fetches all tags' do
- stub_feature_flags(find_tag_via_gitaly: false)
-
- expect(Gitlab::GitalyClient)
- .to receive(:call).with(anything, :ref_service, :find_all_tags, anything, anything).and_call_original
-
- expect(repository.find_tag('v1.1.0').name).to eq('v1.1.0')
- end
- end
end
describe '#avatar' do
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index b2fa9c24535..0489a4fb995 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Route do
describe 'relationships' do
it { is_expected.to belong_to(:source) }
+ it { is_expected.to belong_to(:namespace) }
end
describe 'validations' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index f8cea619233..ac2474ac393 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -83,6 +83,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil }
+
+ it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
end
describe 'associations' do
@@ -436,7 +439,7 @@ RSpec.describe User do
subject { build(:user) }
end
- it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :public_email, :notification_email do
+ it_behaves_like 'an object with email-formatted attributes', :public_email, :notification_email do
subject { create(:user).tap { |user| user.emails << build(:email, email: email_value, confirmed_at: Time.current) } }
end
@@ -542,6 +545,13 @@ RSpec.describe User do
expect(user).to be_invalid
expect(user.errors.messages[:email].first).to eq(expected_error)
end
+
+ it 'does not allow user to update email to a non-allowlisted domain' do
+ user = create(:user, email: "info@test.example.com")
+
+ expect { user.update!(email: "test@notexample.com") }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
end
context 'when a signup domain is allowed and subdomains are not allowed' do
@@ -608,6 +618,13 @@ RSpec.describe User do
user = build(:user, email: 'info@example.com', created_by_id: 1)
expect(user).to be_valid
end
+
+ it 'does not allow user to update email to a denied domain' do
+ user = create(:user, email: 'info@test.com')
+
+ expect { user.update!(email: 'info@example.com') }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
end
context 'when a signup domain is denied but a wildcard subdomain is allowed' do
@@ -679,6 +696,13 @@ RSpec.describe User do
expect(user.errors.messages[:email].first).to eq(expected_error)
end
+ it 'does not allow user to update email to a restricted domain' do
+ user = create(:user, email: 'info@test.com')
+
+ expect { user.update!(email: 'info@gitlab.com') }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
+
it 'does accept a valid email address' do
user = build(:user, email: 'info@test.com')
@@ -1398,7 +1422,7 @@ RSpec.describe User do
end
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
- let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") }
+ let(:request) { double('request', remote_ip: "127.0.0.1") }
let(:user) { create(:user) }
it 'writes trackable attributes' do
@@ -1481,27 +1505,176 @@ RSpec.describe User do
end
describe '#confirm' do
+ let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at) { Date.today }
+
before do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
end
- let(:user) { create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com') }
+ let(:user) do
+ create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user|
+ user.update!(confirmation_sent_at: confirmation_sent_at)
+ end
+ end
- it 'returns unconfirmed' do
- expect(user.confirmed?).to be_falsey
+ shared_examples_for 'unconfirmed user' do
+ it 'returns unconfirmed' do
+ expect(user.confirmed?).to be_falsey
+ end
end
- it 'confirms a user' do
- user.confirm
- expect(user.confirmed?).to be_truthy
+ context 'when the confirmation period has expired' do
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed user'
+
+ it 'does not confirm the user' do
+ user.confirm
+
+ expect(user.confirmed?).to be_falsey
+ end
+
+ it 'does not add the confirmed primary email to emails' do
+ user.confirm
+
+ expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
+ end
end
- it 'adds the confirmed primary email to emails' do
- expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
+ context 'when the confirmation period has not expired' do
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
- user.confirm
+ it_behaves_like 'unconfirmed user'
- expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ it 'confirms a user' do
+ user.confirm
+ expect(user.confirmed?).to be_truthy
+ end
+
+ it 'adds the confirmed primary email to emails' do
+ expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
+
+ user.confirm
+
+ expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ end
+
+ context 'when the primary email is already included in user.emails' do
+ let(:expired_confirmation_sent_at_for_email) { Date.today - Email.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at_for_email) { Date.today }
+
+ let!(:email) do
+ create(:email, email: user.unconfirmed_email, user: user).tap do |email|
+ email.update!(confirmation_sent_at: confirmation_sent_at_for_email)
+ end
+ end
+
+ context 'when the confirmation period of the email record has expired' do
+ let(:confirmation_sent_at_for_email) { expired_confirmation_sent_at_for_email }
+
+ it 'does not confirm the email record' do
+ user.confirm
+
+ expect(email.reload.confirmed?).to be_falsey
+ end
+ end
+
+ context 'when the confirmation period of the email record has not expired' do
+ let(:confirmation_sent_at_for_email) { extant_confirmation_sent_at_for_email }
+
+ it 'confirms the email record' do
+ user.confirm
+
+ expect(email.reload.confirmed?).to be_truthy
+ end
+ end
+ end
+ end
+ end
+
+ describe '#force_confirm' do
+ let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at) { Date.today }
+
+ let(:user) do
+ create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user|
+ user.update!(confirmation_sent_at: confirmation_sent_at)
+ end
+ end
+
+ shared_examples_for 'unconfirmed user' do
+ it 'returns unconfirmed' do
+ expect(user.confirmed?).to be_falsey
+ end
+ end
+
+ shared_examples_for 'confirms the user on force_confirm' do
+ it 'confirms a user' do
+ user.force_confirm
+ expect(user.confirmed?).to be_truthy
+ end
+ end
+
+ shared_examples_for 'adds the confirmed primary email to emails' do
+ it 'adds the confirmed primary email to emails' do
+ expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
+
+ user.force_confirm
+
+ expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ end
+ end
+
+ shared_examples_for 'confirms the email record if the primary email was already present in user.emails' do
+ context 'when the primary email is already included in user.emails' do
+ let(:expired_confirmation_sent_at_for_email) { Date.today - Email.confirm_within - 7.days }
+ let(:extant_confirmation_sent_at_for_email) { Date.today }
+
+ let!(:email) do
+ create(:email, email: user.unconfirmed_email, user: user).tap do |email|
+ email.update!(confirmation_sent_at: confirmation_sent_at_for_email)
+ end
+ end
+
+ shared_examples_for 'confirms the email record' do
+ it 'confirms the email record' do
+ user.force_confirm
+
+ expect(email.reload.confirmed?).to be_truthy
+ end
+ end
+
+ context 'when the confirmation period of the email record has expired' do
+ let(:confirmation_sent_at_for_email) { expired_confirmation_sent_at_for_email }
+
+ it_behaves_like 'confirms the email record'
+ end
+
+ context 'when the confirmation period of the email record has not expired' do
+ let(:confirmation_sent_at_for_email) { extant_confirmation_sent_at_for_email }
+
+ it_behaves_like 'confirms the email record'
+ end
+ end
+ end
+
+ context 'when the confirmation period has expired' do
+ let(:confirmation_sent_at) { expired_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed user'
+ it_behaves_like 'confirms the user on force_confirm'
+ it_behaves_like 'adds the confirmed primary email to emails'
+ it_behaves_like 'confirms the email record if the primary email was already present in user.emails'
+ end
+
+ context 'when the confirmation period has not expired' do
+ let(:confirmation_sent_at) { extant_confirmation_sent_at }
+
+ it_behaves_like 'unconfirmed user'
+ it_behaves_like 'confirms the user on force_confirm'
+ it_behaves_like 'adds the confirmed primary email to emails'
+ it_behaves_like 'confirms the email record if the primary email was already present in user.emails'
end
end
@@ -1523,9 +1696,9 @@ RSpec.describe User do
describe '#generate_password' do
it 'does not generate password by default' do
- user = create(:user, password: 'abcdefghe')
+ user = create(:user, password: Gitlab::Password.test_default)
- expect(user.password).to eq('abcdefghe')
+ expect(user.password).to eq(Gitlab::Password.test_default)
end
end
@@ -1624,6 +1797,29 @@ RSpec.describe User do
expect(static_object_token).not_to be_blank
expect(user.reload.static_object_token).to eq static_object_token
end
+
+ it 'generates an encrypted version of the token' do
+ user = create(:user, static_object_token: nil)
+
+ expect(user[:static_object_token]).to be_nil
+ expect(user[:static_object_token_encrypted]).to be_nil
+
+ user.static_object_token
+
+ expect(user[:static_object_token]).to be_nil
+ expect(user[:static_object_token_encrypted]).to be_present
+ end
+
+ it 'prefers an encoded version of the token' do
+ user = create(:user, static_object_token: nil)
+
+ token = user.static_object_token
+
+ user.update_column(:static_object_token, 'Test')
+
+ expect(user.static_object_token).not_to eq('Test')
+ expect(user.static_object_token).to eq(token)
+ end
end
describe 'enabled_static_object_token' do
@@ -1862,7 +2058,7 @@ RSpec.describe User do
it { expect(user.authorized_groups).to eq([group]) }
it { expect(user.owned_groups).to eq([group]) }
it { expect(user.namespaces).to contain_exactly(user.namespace, group) }
- it { expect(user.manageable_namespaces).to contain_exactly(user.namespace, group) }
+ it { expect(user.forkable_namespaces).to contain_exactly(user.namespace, group) }
context 'with owned groups only' do
before do
@@ -1876,9 +2072,12 @@ RSpec.describe User do
context 'with child groups' do
let!(:subgroup) { create(:group, parent: group) }
- describe '#manageable_namespaces' do
- it 'includes all the namespaces the user can manage' do
- expect(user.manageable_namespaces).to contain_exactly(user.namespace, group, subgroup)
+ describe '#forkable_namespaces' do
+ it 'includes all the namespaces the user can fork into' do
+ developer_group = create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ developer_group.add_developer(user)
+
+ expect(user.forkable_namespaces).to contain_exactly(user.namespace, group, subgroup, developer_group)
end
end
@@ -2592,6 +2791,12 @@ RSpec.describe User do
end
end
+ describe '.user_search_minimum_char_limit' do
+ it 'returns true' do
+ expect(described_class.user_search_minimum_char_limit).to be(true)
+ end
+ end
+
describe '.find_by_ssh_key_id' do
let_it_be(:user) { create(:user) }
let_it_be(:key) { create(:key, user: user) }
@@ -3768,7 +3973,7 @@ RSpec.describe User do
end
end
- describe '#ci_owned_runners' do
+ shared_context '#ci_owned_runners' do
let(:user) { create(:user) }
shared_examples :nested_groups_owner do
@@ -4075,6 +4280,16 @@ RSpec.describe User do
end
end
+ it_behaves_like '#ci_owned_runners'
+
+ context 'when FF ci_owned_runners_cross_joins_fix is disabled' do
+ before do
+ stub_feature_flags(ci_owned_runners_cross_joins_fix: false)
+ end
+
+ it_behaves_like '#ci_owned_runners'
+ end
+
describe '#projects_with_reporter_access_limited_to' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
@@ -5606,6 +5821,48 @@ RSpec.describe User do
end
end
+ describe '#can_log_in_with_non_expired_password?' do
+ let(:user) { build(:user) }
+
+ subject { user.can_log_in_with_non_expired_password? }
+
+ context 'when user can log in' do
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+
+ context 'when user with expired password' do
+ before do
+ user.password_expires_at = 2.minutes.ago
+ end
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+
+ context 'when password expiration is not applicable' do
+ context 'when ldap user' do
+ let(:user) { build(:omniauth_user, provider: 'ldap') }
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user cannot log in' do
+ context 'when user is blocked' do
+ let(:user) { build(:user, :blocked) }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+ end
+
describe '#read_only_attribute?' do
context 'when synced attributes metadata is present' do
it 'delegates to synced_attributes_metadata' do
@@ -6303,13 +6560,43 @@ RSpec.describe User do
specify { is_expected.to contain_exactly(developer_group2) }
end
- describe '.get_ids_by_username' do
+ describe '.get_ids_by_ids_or_usernames' do
let(:user_name) { 'user_name' }
let!(:user) { create(:user, username: user_name) }
let(:user_id) { user.id }
it 'returns the id of each record matching username' do
- expect(described_class.get_ids_by_username([user_name])).to match_array([user_id])
+ expect(described_class.get_ids_by_ids_or_usernames(nil, [user_name])).to match_array([user_id])
+ end
+
+ it 'returns the id of each record matching user id' do
+ expect(described_class.get_ids_by_ids_or_usernames([user_id], nil)).to match_array([user_id])
+ end
+
+ it 'return the id for all records matching either user id or user name' do
+ new_user_id = create(:user).id
+
+ expect(described_class.get_ids_by_ids_or_usernames([new_user_id], [user_name])).to match_array([user_id, new_user_id])
+ end
+ end
+
+ describe '.by_ids_or_usernames' do
+ let(:user_name) { 'user_name' }
+ let!(:user) { create(:user, username: user_name) }
+ let(:user_id) { user.id }
+
+ it 'returns matching records based on username' do
+ expect(described_class.by_ids_or_usernames(nil, [user_name])).to match_array([user])
+ end
+
+ it 'returns matching records based on id' do
+ expect(described_class.by_ids_or_usernames([user_id], nil)).to match_array([user])
+ end
+
+ it 'returns matching records based on both username and id' do
+ new_user = create(:user)
+
+ expect(described_class.by_ids_or_usernames([new_user.id], [user_name])).to match_array([user, new_user])
end
end
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index 8553d0bfdb0..add9bd18755 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe UsersStatistics do
create_list(:user, 2, :bot)
create_list(:user, 1, :blocked)
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(described_class.connection).to receive(:transaction_open?).and_return(false)
end
context 'when successful' do
diff --git a/spec/models/work_item/type_spec.rb b/spec/models/work_items/type_spec.rb
index cc18558975b..6e9f3210e65 100644
--- a/spec/models/work_item/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItem::Type do
+RSpec.describe WorkItems::Type do
describe 'modules' do
it { is_expected.to include_module(CacheMarkdownField) }
end
@@ -12,6 +12,22 @@ RSpec.describe WorkItem::Type do
it { is_expected.to belong_to(:namespace) }
end
+ describe 'scopes' do
+ describe 'order_by_name_asc' do
+ subject { described_class.order_by_name_asc.pluck(:name) }
+
+ before do
+ # Deletes all so we have control on the entire list of names
+ described_class.delete_all
+ create(:work_item_type, name: 'Ztype')
+ create(:work_item_type, name: 'atype')
+ create(:work_item_type, name: 'gtype')
+ end
+
+ it { is_expected.to match(%w[atype gtype Ztype]) }
+ end
+ end
+
describe '#destroy' do
let!(:work_item) { create :issue }
@@ -19,10 +35,10 @@ RSpec.describe WorkItem::Type do
it 'deletes type but not unrelated issues' do
type = create(:work_item_type)
- expect(WorkItem::Type.count).to eq(6)
+ expect(WorkItems::Type.count).to eq(6)
expect { type.destroy! }.not_to change(Issue, :count)
- expect(WorkItem::Type.count).to eq(5)
+ expect(WorkItems::Type.count).to eq(5)
end
end
@@ -44,6 +60,22 @@ RSpec.describe WorkItem::Type do
it { is_expected.not_to allow_value('s' * 256).for(:icon_name) }
end
+ describe 'default?' do
+ subject { build(:work_item_type, namespace: namespace).default? }
+
+ context 'when namespace is nil' do
+ let(:namespace) { nil }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when namespace is present' do
+ let(:namespace) { build(:namespace) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#name' do
it 'strips name' do
work_item_type = described_class.new(name: ' label😸 ')
diff --git a/spec/policies/blob_policy_spec.rb b/spec/policies/blob_policy_spec.rb
index daabcd844af..2b0465f3615 100644
--- a/spec/policies/blob_policy_spec.rb
+++ b/spec/policies/blob_policy_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe BlobPolicy do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
- using RSpec::Parameterized::TableSyntax
- let(:project) { create(:project, :repository, project_level) }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+
let(:user) { create_user_from_membership(project, membership) }
let(:blob) { project.repository.blob_at(SeedRepo::FirstCommit::ID, 'README.md') }
@@ -18,8 +18,9 @@ RSpec.describe BlobPolicy do
end
with_them do
- it "grants permission" do
+ it 'grants permission' do
enable_admin_mode!(user) if admin_mode
+ project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_level.to_s))
update_feature_access_level(project, feature_access_level)
if expected_count == 1
diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb
index d283b0ffda5..50774313aae 100644
--- a/spec/policies/group_member_policy_spec.rb
+++ b/spec/policies/group_member_policy_spec.rb
@@ -83,6 +83,23 @@ RSpec.describe GroupMemberPolicy do
specify { expect_allowed(:read_group) }
end
+ context 'with bot user' do
+ let(:current_user) { create(:user, :project_bot) }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ specify { expect_allowed(:read_group, :destroy_project_bot_member) }
+ end
+
+ context 'with anonymous bot user' do
+ let(:current_user) { create(:user, :project_bot) }
+ let(:membership) { guest.members.first }
+
+ specify { expect_disallowed(:read_group, :destroy_project_bot_member) }
+ end
+
context 'with one owner' do
let(:current_user) { owner }
@@ -106,6 +123,7 @@ RSpec.describe GroupMemberPolicy do
end
specify { expect_allowed(*member_related_permissions) }
+ specify { expect_disallowed(:destroy_project_bot_member) }
end
context 'with the group parent' do
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 7822ee2b92e..2607e285a80 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -6,15 +6,11 @@ RSpec.describe GroupPolicy do
include_context 'GroupPolicy context'
context 'public group with no user' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :crm_enabled) }
let(:current_user) { nil }
it do
- expect_allowed(:read_group)
- expect_allowed(:read_crm_organization)
- expect_allowed(:read_crm_contact)
- expect_allowed(:read_counts)
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -24,34 +20,49 @@ RSpec.describe GroupPolicy do
end
end
- context 'with no user and public project' do
- let(:project) { create(:project, :public) }
+ context 'public group with user who is not a member' do
+ let(:group) { create(:group, :public, :crm_enabled) }
+ let(:current_user) { create(:user) }
+
+ it do
+ expect_allowed(*public_permissions)
+ expect_disallowed(:upload_file)
+ expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ expect_disallowed(:read_namespace)
+ end
+ end
+
+ context 'private group that has been invited to a public project and with no user' do
+ let(:project) { create(:project, :public, group: create(:group, :crm_enabled)) }
let(:current_user) { nil }
before do
create(:project_group_link, project: project, group: group)
end
- it { expect_disallowed(:read_group) }
- it { expect_disallowed(:read_crm_organization) }
- it { expect_disallowed(:read_crm_contact) }
- it { expect_disallowed(:read_counts) }
- it { expect_disallowed(*read_group_permissions) }
+ it do
+ expect_disallowed(*public_permissions)
+ expect_disallowed(*reporter_permissions)
+ expect_disallowed(*owner_permissions)
+ end
end
- context 'with foreign user and public project' do
- let(:project) { create(:project, :public) }
+ context 'private group that has been invited to a public project and with a foreign user' do
+ let(:project) { create(:project, :public, group: create(:group, :crm_enabled)) }
let(:current_user) { create(:user) }
before do
create(:project_group_link, project: project, group: group)
end
- it { expect_disallowed(:read_group) }
- it { expect_disallowed(:read_crm_organization) }
- it { expect_disallowed(:read_crm_contact) }
- it { expect_disallowed(:read_counts) }
- it { expect_disallowed(*read_group_permissions) }
+ it do
+ expect_disallowed(*public_permissions)
+ expect_disallowed(*reporter_permissions)
+ expect_disallowed(*owner_permissions)
+ end
end
context 'has projects' do
@@ -62,13 +73,13 @@ RSpec.describe GroupPolicy do
project.add_developer(current_user)
end
- it { expect_allowed(*read_group_permissions) }
+ it { expect_allowed(*(public_permissions - [:read_counts])) }
context 'in subgroups' do
- let(:subgroup) { create(:group, :private, parent: group) }
+ let(:subgroup) { create(:group, :private, :crm_enabled, parent: group) }
let(:project) { create(:project, namespace: subgroup) }
- it { expect_allowed(*read_group_permissions) }
+ it { expect_allowed(*(public_permissions - [:read_counts])) }
end
end
@@ -81,7 +92,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { deploy_token }
it do
- expect_disallowed(*read_group_permissions)
+ expect_disallowed(*public_permissions)
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -94,7 +105,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -111,7 +122,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -128,7 +139,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -156,7 +167,7 @@ RSpec.describe GroupPolicy do
updated_owner_permissions =
owner_permissions - create_subgroup_permission
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -167,7 +178,7 @@ RSpec.describe GroupPolicy do
context 'with subgroup_creation_level set to owner' do
it 'allows every maintainer permission' do
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -185,7 +196,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { owner }
it do
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -202,7 +213,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { admin }
specify do
- expect_disallowed(*read_group_permissions)
+ expect_disallowed(*public_permissions)
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -212,7 +223,7 @@ RSpec.describe GroupPolicy do
context 'with admin mode', :enable_admin_mode do
specify do
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -233,7 +244,7 @@ RSpec.describe GroupPolicy do
describe 'private nested group use the highest access level from the group and inherited permissions' do
let_it_be(:nested_group) do
- create(:group, :private, :owner_subgroup_creation_only, parent: group)
+ create(:group, :private, :owner_subgroup_creation_only, :crm_enabled, parent: group)
end
before_all do
@@ -254,8 +265,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { nil }
it do
- expect_disallowed(:read_counts)
- expect_disallowed(*read_group_permissions)
+ expect_disallowed(*public_permissions)
expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -268,8 +278,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(:read_counts)
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -282,8 +291,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(:read_counts)
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
@@ -296,8 +304,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(:read_counts)
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -310,8 +317,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { maintainer }
it do
- expect_allowed(:read_counts)
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -324,8 +330,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { owner }
it do
- expect_allowed(:read_counts)
- expect_allowed(*read_group_permissions)
+ expect_allowed(*public_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
@@ -340,7 +345,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { owner }
context 'when the group share_with_group_lock is enabled' do
- let(:group) { create(:group, share_with_group_lock: true, parent: parent) }
+ let(:group) { create(:group, :crm_enabled, share_with_group_lock: true, parent: parent) }
before do
group.add_owner(owner)
@@ -348,10 +353,10 @@ RSpec.describe GroupPolicy do
context 'when the parent group share_with_group_lock is enabled' do
context 'when the group has a grandparent' do
- let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) }
+ let(:parent) { create(:group, :crm_enabled, share_with_group_lock: true, parent: grandparent) }
context 'when the grandparent share_with_group_lock is enabled' do
- let(:grandparent) { create(:group, share_with_group_lock: true) }
+ let(:grandparent) { create(:group, :crm_enabled, share_with_group_lock: true) }
context 'when the current_user owns the parent' do
before do
@@ -377,7 +382,7 @@ RSpec.describe GroupPolicy do
end
context 'when the grandparent share_with_group_lock is disabled' do
- let(:grandparent) { create(:group) }
+ let(:grandparent) { create(:group, :crm_enabled) }
context 'when the current_user owns the parent' do
before do
@@ -394,7 +399,7 @@ RSpec.describe GroupPolicy do
end
context 'when the group does not have a grandparent' do
- let(:parent) { create(:group, share_with_group_lock: true) }
+ let(:parent) { create(:group, :crm_enabled, share_with_group_lock: true) }
context 'when the current_user owns the parent' do
before do
@@ -411,7 +416,7 @@ RSpec.describe GroupPolicy do
end
context 'when the parent group share_with_group_lock is disabled' do
- let(:parent) { create(:group) }
+ let(:parent) { create(:group, :crm_enabled) }
it { expect_allowed(:change_share_with_group_lock) }
end
@@ -696,7 +701,7 @@ RSpec.describe GroupPolicy do
end
it_behaves_like 'clusterable policies' do
- let(:clusterable) { create(:group) }
+ let(:clusterable) { create(:group, :crm_enabled) }
let(:cluster) do
create(:cluster,
:provided_by_gcp,
@@ -706,7 +711,7 @@ RSpec.describe GroupPolicy do
end
describe 'update_max_artifacts_size' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :crm_enabled) }
context 'when no user' do
let(:current_user) { nil }
@@ -736,7 +741,7 @@ RSpec.describe GroupPolicy do
end
describe 'design activity' do
- let_it_be(:group) { create(:group, :public) }
+ let_it_be(:group) { create(:group, :public, :crm_enabled) }
let(:current_user) { nil }
@@ -904,7 +909,6 @@ RSpec.describe GroupPolicy do
context 'feature enabled' do
before do
stub_config(dependency_proxy: { enabled: true })
- group.create_dependency_proxy_setting!(enabled: true)
end
context 'reporter' do
@@ -933,8 +937,6 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) }
- it { is_expected.to be_allowed(:read_crm_organization) }
- it { is_expected.to be_allowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:create_package) }
end
@@ -944,8 +946,6 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:create_package) }
it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) }
- it { is_expected.to be_allowed(:read_crm_organization) }
- it { is_expected.to be_allowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:destroy_package) }
end
@@ -954,7 +954,6 @@ RSpec.describe GroupPolicy do
before do
stub_config(dependency_proxy: { enabled: true })
- group.create_dependency_proxy_setting!(enabled: true)
end
it { is_expected.to be_allowed(:read_dependency_proxy) }
@@ -965,7 +964,7 @@ RSpec.describe GroupPolicy do
it_behaves_like 'Self-managed Core resource access tokens'
context 'support bot' do
- let_it_be(:group) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, :crm_enabled) }
let_it_be(:current_user) { User.support_bot }
before do
@@ -975,7 +974,7 @@ RSpec.describe GroupPolicy do
it { expect_disallowed(:read_label) }
context 'when group hierarchy has a project with service desk enabled' do
- let_it_be(:subgroup) { create(:group, :private, parent: group)}
+ let_it_be(:subgroup) { create(:group, :private, :crm_enabled, parent: group) }
let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) }
it { expect_allowed(:read_label) }
@@ -983,6 +982,49 @@ RSpec.describe GroupPolicy do
end
end
+ context "project bots" do
+ let(:project_bot) { create(:user, :project_bot) }
+ let(:user) { create(:user) }
+
+ context "project_bot_access" do
+ context "when regular user and part of the group" do
+ let(:current_user) { user }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it { is_expected.not_to be_allowed(:project_bot_access) }
+ end
+
+ context "when project bot and not part of the project" do
+ let(:current_user) { project_bot }
+
+ it { is_expected.not_to be_allowed(:project_bot_access) }
+ end
+
+ context "when project bot and part of the project" do
+ let(:current_user) { project_bot }
+
+ before do
+ group.add_developer(project_bot)
+ end
+
+ it { is_expected.to be_allowed(:project_bot_access) }
+ end
+ end
+
+ context 'with resource access tokens' do
+ let(:current_user) { project_bot }
+
+ before do
+ group.add_maintainer(project_bot)
+ end
+
+ it { is_expected.not_to be_allowed(:create_resource_access_tokens) }
+ end
+ end
+
describe 'update_runners_registration_token' do
context 'admin' do
let(:current_user) { admin }
@@ -1083,9 +1125,7 @@ RSpec.describe GroupPolicy do
context 'with maintainer' do
let(:current_user) { maintainer }
- it { is_expected.to be_allowed(:register_group_runners) }
-
- it_behaves_like 'expected outcome based on runner registration control'
+ it { is_expected.to be_disallowed(:register_group_runners) }
end
context 'with reporter' do
@@ -1113,7 +1153,7 @@ RSpec.describe GroupPolicy do
end
end
- context 'with customer_relations feature flag disabled' do
+ context 'with customer relations feature flag disabled' do
let(:current_user) { owner }
before do
@@ -1125,4 +1165,18 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_disallowed(:admin_crm_contact) }
it { is_expected.to be_disallowed(:admin_crm_organization) }
end
+
+ context 'when crm_enabled is false' do
+ let(:current_user) { owner }
+
+ before_all do
+ group.crm_settings.enabled = false
+ group.crm_settings.save!
+ end
+
+ it { is_expected.to be_disallowed(:read_crm_contact) }
+ it { is_expected.to be_disallowed(:read_crm_organization) }
+ it { is_expected.to be_disallowed(:admin_crm_contact) }
+ it { is_expected.to be_disallowed(:admin_crm_organization) }
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 2953c198af6..38e4e18c894 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe ProjectPolicy do
end
it 'does not include the issues permissions' do
- expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident
+ expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
end
it 'disables boards and lists permissions' do
@@ -73,7 +73,7 @@ RSpec.describe ProjectPolicy do
it 'does not include the issues permissions' do
create(:jira_integration, project: project)
- expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident
+ expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
end
end
end
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 8c0347b3c8d..3bf592ed2b9 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -53,6 +53,10 @@ RSpec.describe BlobPresenter do
end
end
+ describe '#archived?' do
+ it { expect(presenter.archived?).to eq(project.archived) }
+ end
+
describe '#pipeline_editor_path' do
context 'when blob is .gitlab-ci.yml' do
before do
@@ -67,6 +71,22 @@ RSpec.describe BlobPresenter do
end
end
+ describe '#find_file_path' do
+ it { expect(presenter.find_file_path).to eq("/#{project.full_path}/-/find_file/HEAD/files/ruby/regex.rb") }
+ end
+
+ describe '#blame_path' do
+ it { expect(presenter.blame_path).to eq("/#{project.full_path}/-/blame/HEAD/files/ruby/regex.rb") }
+ end
+
+ describe '#history_path' do
+ it { expect(presenter.history_path).to eq("/#{project.full_path}/-/commits/HEAD/files/ruby/regex.rb") }
+ end
+
+ describe '#permalink_path' do
+ it { expect(presenter.permalink_path).to eq("/#{project.full_path}/-/blob/#{project.repository.commit.sha}/files/ruby/regex.rb") }
+ end
+
describe '#code_owners' do
it { expect(presenter.code_owners).to match_array([]) }
end
diff --git a/spec/presenters/label_presenter_spec.rb b/spec/presenters/label_presenter_spec.rb
index bab0d9a1065..b4d36eaf340 100644
--- a/spec/presenters/label_presenter_spec.rb
+++ b/spec/presenters/label_presenter_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe LabelPresenter do
let(:label) { build_stubbed(:label, project: project).present(issuable_subject: project) }
let(:group_label) { build_stubbed(:group_label, group: group).present(issuable_subject: project) }
+ let(:admin_label) { build_stubbed(:admin_label).present(issuable_subject: nil) }
describe '#edit_path' do
context 'with group label' do
@@ -23,6 +24,12 @@ RSpec.describe LabelPresenter do
it { is_expected.to eq(edit_project_label_path(project, label)) }
end
+
+ context 'with an admin label' do
+ subject { admin_label.edit_path }
+
+ it { is_expected.to eq(edit_admin_label_path(admin_label)) }
+ end
end
describe '#destroy_path' do
@@ -37,6 +44,12 @@ RSpec.describe LabelPresenter do
it { is_expected.to eq(project_label_path(project, label)) }
end
+
+ context 'with an admin label' do
+ subject { admin_label.destroy_path }
+
+ it { is_expected.to eq(admin_label_path(admin_label)) }
+ end
end
describe '#filter_path' do
@@ -91,6 +104,12 @@ RSpec.describe LabelPresenter do
it { is_expected.to eq(label.project.name) }
end
+
+ context 'with an admin label' do
+ subject { admin_label.subject_name }
+
+ it { is_expected.to be_nil }
+ end
end
describe '#subject_full_name' do
@@ -105,5 +124,11 @@ RSpec.describe LabelPresenter do
it { is_expected.to eq(label.project.full_name) }
end
+
+ context 'with an admin label' do
+ subject { admin_label.subject_full_name }
+
+ it { is_expected.to be_nil }
+ end
end
end
diff --git a/spec/presenters/packages/conan/package_presenter_spec.rb b/spec/presenters/packages/conan/package_presenter_spec.rb
index 6d82c5ef547..27ecf32b6f2 100644
--- a/spec/presenters/packages/conan/package_presenter_spec.rb
+++ b/spec/presenters/packages/conan/package_presenter_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
let_it_be(:conan_package_reference) { '123456789'}
let(:params) { { package_scope: :instance } }
+ let(:presenter) { described_class.new(package, user, project, params) }
shared_examples 'no existing package' do
context 'when package does not exist' do
@@ -21,7 +22,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
shared_examples 'conan_file_metadatum is not found' do
context 'when no conan_file_metadatum exists' do
before do
- package.package_files.each do |file|
+ package.installable_package_files.each do |file|
file.conan_file_metadatum.delete
file.reload
end
@@ -32,7 +33,7 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
describe '#recipe_urls' do
- subject { described_class.new(package, user, project, params).recipe_urls }
+ subject { presenter.recipe_urls }
it_behaves_like 'no existing package'
it_behaves_like 'conan_file_metadatum is not found'
@@ -71,7 +72,9 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
describe '#recipe_snapshot' do
- subject { described_class.new(package, user, project).recipe_snapshot }
+ let(:params) { {} }
+
+ subject { presenter.recipe_snapshot }
it_behaves_like 'no existing package'
it_behaves_like 'conan_file_metadatum is not found'
@@ -180,12 +183,9 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
describe '#package_snapshot' do
let(:reference) { conan_package_reference }
+ let(:params) { { conan_package_reference: reference } }
- subject do
- described_class.new(
- package, user, project, conan_package_reference: reference
- ).package_snapshot
- end
+ subject { presenter.package_snapshot }
it_behaves_like 'no existing package'
it_behaves_like 'conan_file_metadatum is not found'
@@ -208,4 +208,22 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
end
end
end
+
+ # TODO when cleaning up packages_installable_package_files, consider removing this context and
+ # add a dummy package file pending destruction on L8
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ subject { presenter.send(:package_files).to_a }
+
+ it { is_expected.not_to include(package_file_pending_destruction) }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include(package_file_pending_destruction) }
+ end
+ end
end
diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb
index 3009f2bd56d..4e2645b27ff 100644
--- a/spec/presenters/packages/detail/package_presenter_spec.rb
+++ b/spec/presenters/packages/detail/package_presenter_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, creator: user) }
let_it_be(:package) { create(:npm_package, :with_build, project: project) }
- let(:presenter) { described_class.new(package) }
-
let_it_be(:user_info) { { name: user.name, avatar_url: user.avatar_url } }
+ let(:presenter) { described_class.new(package) }
+
let!(:expected_package_files) do
- package.package_files.map do |file|
+ package.installable_package_files.map do |file|
{
created_at: file.created_at,
download_path: file.download_path,
@@ -154,5 +154,21 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
expect(presenter.detail_view).to eq expected_package_details
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ subject { presenter.detail_view[:package_files].map { |e| e[:id] } }
+
+ it { is_expected.not_to include(package_file_pending_destruction.id) }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include(package_file_pending_destruction.id) }
+ end
+ end
end
end
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index 3b6dfcd20b8..2308f928c92 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -95,6 +95,27 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
end
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package2, file_sha1: 'pending_destruction_sha1') }
+
+ let(:shasums) { subject.values.map { |v| v.dig(:dist, :shasum) } }
+
+ it 'does not return them' do
+ expect(shasums).not_to include(package_file_pending_destruction.file_sha1)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ package2.package_files.id_not_in(package_file_pending_destruction.id).delete_all
+ end
+
+ it 'returns them' do
+ expect(shasums).to include(package_file_pending_destruction.file_sha1)
+ end
+ end
+ end
end
describe '#dist_tags' do
diff --git a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
index 8bb0694f39c..6e99b6bafec 100644
--- a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
@@ -24,6 +24,20 @@ RSpec.describe Packages::Nuget::PackageMetadataPresenter do
subject { presenter.archive_url }
it { is_expected.to end_with(expected_suffix) }
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package, file_name: 'pending_destruction.nupkg') }
+
+ it { is_expected.not_to include('pending_destruction.nupkg') }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include('pending_destruction.nupkg') }
+ end
+ end
end
describe '#catalog_entry' do
diff --git a/spec/presenters/packages/nuget/search_results_presenter_spec.rb b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
index 39ec7251dfd..745914c6c43 100644
--- a/spec/presenters/packages/nuget/search_results_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter do
let_it_be(:tag2) { create(:packages_tag, package: package_a, name: 'tag2') }
let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') }
let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') }
- let_it_be(:search_results) { OpenStruct.new(total_count: 3, results: [package_a, packages_b, packages_c].flatten) }
- let_it_be(:presenter) { described_class.new(search_results) }
+ let(:search_results) { double('search_results', total_count: 3, results: [package_a, packages_b, packages_c].flatten) }
+ let(:presenter) { described_class.new(search_results) }
let(:total_count) { presenter.total_count }
let(:data) { presenter.data }
diff --git a/spec/presenters/packages/pypi/package_presenter_spec.rb b/spec/presenters/packages/pypi/package_presenter_spec.rb
index 25aa5c31034..8a23c0ec3cb 100644
--- a/spec/presenters/packages/pypi/package_presenter_spec.rb
+++ b/spec/presenters/packages/pypi/package_presenter_spec.rb
@@ -52,5 +52,21 @@ RSpec.describe ::Packages::Pypi::PackagePresenter do
it_behaves_like 'pypi package presenter'
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package1, file_name: "package_file_pending_destruction") }
+
+ let(:project_or_group) { project }
+
+ it { is_expected.not_to include(package_file_pending_destruction.file_name)}
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to include(package_file_pending_destruction.file_name)}
+ end
+ end
end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 27b777dec5f..e4a08bd56c8 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -554,7 +554,7 @@ RSpec.describe ProjectPresenter do
expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(
is_link: false,
label: a_string_including('Add Kubernetes cluster'),
- link: presenter.new_project_cluster_path(project)
+ link: presenter.project_clusters_path(project)
)
end
end
diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb
index 836753d0483..f9150179ae5 100644
--- a/spec/presenters/projects/security/configuration_presenter_spec.rb
+++ b/spec/presenters/projects/security/configuration_presenter_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
before do
stub_licensed_features(licensed_scan_types.to_h { |type| [type, true] })
- stub_feature_flags(corpus_management: false)
+ stub_feature_flags(corpus_management_ui: false)
end
describe '#to_html_data_attribute' do
diff --git a/spec/presenters/service_hook_presenter_spec.rb b/spec/presenters/service_hook_presenter_spec.rb
index 7d7b71f324a..25ded17fb34 100644
--- a/spec/presenters/service_hook_presenter_spec.rb
+++ b/spec/presenters/service_hook_presenter_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe ServiceHookPresenter do
subject { service_hook.present.logs_details_path(web_hook_log) }
let(:expected_path) do
- "/#{project.namespace.path}/#{project.name}/-/services/#{integration.to_param}/hook_logs/#{web_hook_log.id}"
+ "/#{project.namespace.path}/#{project.name}/-/integrations/#{integration.to_param}/hook_logs/#{web_hook_log.id}"
end
it { is_expected.to eq(expected_path) }
@@ -22,7 +22,7 @@ RSpec.describe ServiceHookPresenter do
subject { service_hook.present.logs_retry_path(web_hook_log) }
let(:expected_path) do
- "/#{project.namespace.path}/#{project.name}/-/services/#{integration.to_param}/hook_logs/#{web_hook_log.id}/retry"
+ "/#{project.namespace.path}/#{project.name}/-/integrations/#{integration.to_param}/hook_logs/#{web_hook_log.id}/retry"
end
it { is_expected.to eq(expected_path) }
diff --git a/spec/presenters/web_hook_log_presenter_spec.rb b/spec/presenters/web_hook_log_presenter_spec.rb
index aa9d1d8f545..5827f3378de 100644
--- a/spec/presenters/web_hook_log_presenter_spec.rb
+++ b/spec/presenters/web_hook_log_presenter_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe WebHookLogPresenter do
let(:web_hook) { create(:service_hook, integration: integration) }
let(:integration) { create(:drone_ci_integration, project: project) }
- it { is_expected.to eq(project_service_hook_log_path(project, integration, web_hook_log)) }
+ it { is_expected.to eq(project_integration_hook_log_path(project, integration, web_hook_log)) }
end
end
@@ -41,7 +41,7 @@ RSpec.describe WebHookLogPresenter do
let(:web_hook) { create(:service_hook, integration: integration) }
let(:integration) { create(:drone_ci_integration, project: project) }
- it { is_expected.to eq(retry_project_service_hook_log_path(project, integration, web_hook_log)) }
+ it { is_expected.to eq(retry_project_integration_hook_log_path(project, integration, web_hook_log)) }
end
end
end
diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb
index ca5b4d8337c..0386fef5134 100644
--- a/spec/rake_helper.rb
+++ b/spec/rake_helper.rb
@@ -11,7 +11,7 @@ RSpec.configure do |config|
Rake::Task.define_task :environment
end
- config.after(:all) do
+ config.after(:all, type: :task) do
delete_from_all_tables!(except: deletion_except_tables)
end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 585fab33708..0db6acbc7b8 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -81,6 +81,71 @@ RSpec.describe API::Ci::JobArtifacts do
end
end
+ describe 'DELETE /projects/:id/artifacts' do
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(bulk_expire_project_artifacts: false)
+ end
+
+ it 'returns 404' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is anonymous' do
+ let(:api_user) { nil }
+
+ it 'does not execute Ci::JobArtifacts::DeleteProjectArtifactsService' do
+ expect(Ci::JobArtifacts::DeleteProjectArtifactsService)
+ .not_to receive(:new)
+
+ delete api("/projects/#{project.id}/artifacts", api_user)
+ end
+
+ it 'returns status 401 (unauthorized)' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with developer' do
+ it 'does not execute Ci::JobArtifacts::DeleteProjectArtifactsService' do
+ expect(Ci::JobArtifacts::DeleteProjectArtifactsService)
+ .not_to receive(:new)
+
+ delete api("/projects/#{project.id}/artifacts", api_user)
+ end
+
+ it 'returns status 403 (forbidden)' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'with authorized user' do
+ let(:maintainer) { create(:project_member, :maintainer, project: project).user }
+ let!(:api_user) { maintainer }
+
+ it 'executes Ci::JobArtifacts::DeleteProjectArtifactsService' do
+ expect_next_instance_of(Ci::JobArtifacts::DeleteProjectArtifactsService, project: project) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ delete api("/projects/#{project.id}/artifacts", api_user)
+ end
+
+ it 'returns status 202 (accepted)' do
+ delete api("/projects/#{project.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do
context 'when job has artifacts' do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index a51d8b458f8..530b601add9 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -3,21 +3,6 @@
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
- include StubGitlabCalls
- include RedisHelpers
- include WorkhorseHelpers
-
- let(:registration_token) { 'abcdefg123456' }
-
- before do
- stub_feature_flags(ci_enable_live_trace: true)
- stub_feature_flags(runner_registration_control: false)
- stub_gitlab_calls
- stub_application_setting(runners_registration_token: registration_token)
- stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
- allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
- end
-
describe '/api/v4/runners' do
describe 'POST /api/v4/runners' do
context 'when no token is provided' do
@@ -30,380 +15,108 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when invalid token is provided' do
it 'returns 403 error' do
+ allow_next_instance_of(::Ci::RegisterRunnerService) do |service|
+ allow(service).to receive(:execute).and_return(nil)
+ end
+
post api('/runners'), params: { token: 'invalid' }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
- context 'when valid token is provided' do
+ context 'when valid parameters are provided' do
def request
- post api('/runners'), params: { token: token }
- end
-
- context 'with a registration token' do
- let(:token) { registration_token }
-
- it 'creates runner with default values' do
- request
-
- runner = ::Ci::Runner.first
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['id']).to eq(runner.id)
- expect(json_response['token']).to eq(runner.token)
- expect(runner.run_untagged).to be true
- expect(runner.active).to be true
- expect(runner.token).not_to eq(registration_token)
- expect(runner).to be_instance_type
- end
-
- it_behaves_like 'storing arguments in the application context for the API' do
- subject { request }
-
- let(:expected_params) { { client_id: "runner/#{::Ci::Runner.first.id}" } }
- end
-
- it_behaves_like 'not executing any extra queries for the application context' do
- let(:subject_proc) { proc { request } }
- end
- end
-
- context 'when project token is used' do
- let(:project) { create(:project) }
- let(:token) { project.runners_token }
-
- it 'creates project runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(project.runners.size).to eq(1)
- runner = ::Ci::Runner.first
- expect(runner.token).not_to eq(registration_token)
- expect(runner.token).not_to eq(project.runners_token)
- expect(runner).to be_project_type
- end
-
- it_behaves_like 'storing arguments in the application context for the API' do
- subject { request }
-
- let(:expected_params) { { project: project.full_path, client_id: "runner/#{::Ci::Runner.first.id}" } }
- end
-
- it_behaves_like 'not executing any extra queries for the application context' do
- let(:subject_proc) { proc { request } }
- end
-
- context 'when it exceeds the application limits' do
- before do
- create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
- create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
- end
-
- it 'does not create runner' do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include('runner_projects.base' => ['Maximum number of ci registered project runners (1) exceeded'])
- expect(project.runners.reload.size).to eq(1)
- end
- end
-
- context 'when abandoned runners cause application limits to not be exceeded' do
- before do
- create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
- create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
- end
-
- it 'creates runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['message']).to be_nil
- expect(project.runners.reload.size).to eq(2)
- expect(project.runners.recent.size).to eq(1)
- end
- end
-
- context 'when valid runner registrars do not include project' do
- before do
- stub_application_setting(valid_runner_registrars: ['group'])
- end
-
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(runner_registration_control: true)
- end
-
- it 'returns 403 error' do
- request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'when feature flag is disabled' do
- it 'registers the runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be true
- end
- end
- end
- end
-
- context 'when group token is used' do
- let(:group) { create(:group) }
- let(:token) { group.runners_token }
-
- it 'creates a group runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(group.runners.reload.size).to eq(1)
- runner = ::Ci::Runner.first
- expect(runner.token).not_to eq(registration_token)
- expect(runner.token).not_to eq(group.runners_token)
- expect(runner).to be_group_type
- end
-
- it_behaves_like 'storing arguments in the application context for the API' do
- subject { request }
-
- let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{::Ci::Runner.first.id}" } }
- end
-
- it_behaves_like 'not executing any extra queries for the application context' do
- let(:subject_proc) { proc { request } }
- end
-
- context 'when it exceeds the application limits' do
- before do
- create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
- create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
- end
-
- it 'does not create runner' do
- request
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include('runner_namespaces.base' => ['Maximum number of ci registered group runners (1) exceeded'])
- expect(group.runners.reload.size).to eq(1)
- end
- end
-
- context 'when abandoned runners cause application limits to not be exceeded' do
- before do
- create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
- create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
- create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
- end
-
- it 'creates runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['message']).to be_nil
- expect(group.runners.reload.size).to eq(3)
- expect(group.runners.recent.size).to eq(1)
- end
- end
-
- context 'when valid runner registrars do not include group' do
- before do
- stub_application_setting(valid_runner_registrars: ['project'])
- end
-
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(runner_registration_control: true)
- end
-
- it 'returns 403 error' do
- request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'when feature flag is disabled' do
- it 'registers the runner' do
- request
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be true
- end
- end
- end
- end
- end
-
- context 'when runner description is provided' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- description: 'server.hostname'
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.description).to eq('server.hostname')
- end
- end
-
- context 'when runner tags are provided' do
- it 'creates runner' do
post api('/runners'), params: {
- token: registration_token,
- tag_list: 'tag1, tag2'
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
- end
- end
-
- context 'when option for running untagged jobs is provided' do
- context 'when tags are provided' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- run_untagged: false,
- tag_list: ['tag']
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.run_untagged).to be false
- expect(::Ci::Runner.first.tag_list.sort).to eq(['tag'])
- end
- end
-
- context 'when tags are not provided' do
- it 'returns 400 error' do
- post api('/runners'), params: {
- token: registration_token,
- run_untagged: false
- }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include(
- 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs'])
+ token: 'valid token',
+ description: 'server.hostname',
+ maintainer_note: 'Some maintainer notes',
+ run_untagged: false,
+ tag_list: 'tag1, tag2',
+ locked: true,
+ active: true,
+ access_level: 'ref_protected',
+ maximum_timeout: 9000
+ }
+ end
+
+ let_it_be(:new_runner) { create(:ci_runner) }
+
+ before do
+ allow_next_instance_of(::Ci::RegisterRunnerService) do |service|
+ expected_params = {
+ description: 'server.hostname',
+ maintainer_note: 'Some maintainer notes',
+ run_untagged: false,
+ tag_list: %w(tag1 tag2),
+ locked: true,
+ active: true,
+ access_level: 'ref_protected',
+ maximum_timeout: 9000
+ }.stringify_keys
+
+ allow(service).to receive(:execute)
+ .once
+ .with('valid token', a_hash_including(expected_params))
+ .and_return(new_runner)
end
end
- end
- context 'when option for locking Runner is provided' do
it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- locked: true
- }
+ request
expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.locked).to be true
+ expect(json_response['id']).to eq(new_runner.id)
+ expect(json_response['token']).to eq(new_runner.token)
end
- end
- context 'when option for activating a Runner is provided' do
- context 'when active is set to true' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- active: true
- }
+ it_behaves_like 'storing arguments in the application context for the API' do
+ subject { request }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be true
- end
+ let(:expected_params) { { client_id: "runner/#{new_runner.id}" } }
end
- context 'when active is set to false' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- active: false
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.active).to be false
- end
+ it_behaves_like 'not executing any extra queries for the application context' do
+ let(:subject_proc) { proc { request } }
end
end
- context 'when access_level is provided for Runner' do
- context 'when access_level is set to ref_protected' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- access_level: 'ref_protected'
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.ref_protected?).to be true
- end
- end
+ context 'calling actual register service' do
+ include StubGitlabCalls
- context 'when access_level is set to not_protected' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- access_level: 'not_protected'
- }
+ let(:registration_token) { 'abcdefg123456' }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.ref_protected?).to be false
- end
+ before do
+ stub_gitlab_calls
+ stub_application_setting(runners_registration_token: registration_token)
+ allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
- end
-
- context 'when maximum job timeout is specified' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- maximum_timeout: 9000
- }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.maximum_timeout).to eq(9000)
- end
+ %w(name version revision platform architecture).each do |param|
+ context "when info parameter '#{param}' info is present" do
+ let(:value) { "#{param}_value" }
- context 'when maximum job timeout is empty' do
- it 'creates runner' do
- post api('/runners'), params: {
- token: registration_token,
- maximum_timeout: ''
- }
+ it "updates provided Runner's parameter" do
+ post api('/runners'), params: {
+ token: registration_token,
+ info: { param => value }
+ }
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.maximum_timeout).to be_nil
+ expect(response).to have_gitlab_http_status(:created)
+ expect(::Ci::Runner.last.read_attribute(param.to_sym)).to eq(value)
+ end
end
end
- end
- %w(name version revision platform architecture).each do |param|
- context "when info parameter '#{param}' info is present" do
- let(:value) { "#{param}_value" }
+ it "sets the runner's ip_address" do
+ post api('/runners'),
+ params: { token: registration_token },
+ headers: { 'X-Forwarded-For' => '123.111.123.111' }
- it "updates provided Runner's parameter" do
- post api('/runners'), params: {
- token: registration_token,
- info: { param => value }
- }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
- end
+ expect(response).to have_gitlab_http_status(:created)
+ expect(::Ci::Runner.last.ip_address).to eq('123.111.123.111')
end
end
-
- it "sets the runner's ip_address" do
- post api('/runners'),
- params: { token: registration_token },
- headers: { 'X-Forwarded-For' => '123.111.123.111' }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111')
- end
end
end
end
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 6ca380a3cb9..305c0bd9df0 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -980,7 +980,7 @@ RSpec.describe API::Ci::Runners do
end
end
- describe 'GET /groups/:id/runners' do
+ shared_context 'GET /groups/:id/runners' do
context 'authorized user with maintainer privileges' do
it 'returns all runners' do
get api("/groups/#{group.id}/runners", user)
@@ -1048,6 +1048,16 @@ RSpec.describe API::Ci::Runners do
end
end
+ it_behaves_like 'GET /groups/:id/runners'
+
+ context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do
+ before do
+ stub_feature_flags(ci_find_runners_by_ci_mirrors: false)
+ end
+
+ it_behaves_like 'GET /groups/:id/runners'
+ end
+
describe 'POST /projects/:id/runners' do
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb
index d270a16d28d..a036a55f5f3 100644
--- a/spec/requests/api/ci/triggers_spec.rb
+++ b/spec/requests/api/ci/triggers_spec.rb
@@ -162,7 +162,7 @@ RSpec.describe API::Ci::Triggers do
expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"),
params: { ref: 'refs/heads/other-branch' },
- headers: { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
+ headers: { ::Gitlab::WebHooks::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
end.not_to change(Ci::Pipeline, :count)
expect(response).to have_gitlab_http_status(:forbidden)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 1e587480fd9..2bc642f8b14 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1056,9 +1056,7 @@ RSpec.describe API::Commits do
shared_examples_for 'ref with pipeline' do
let!(:pipeline) do
- project
- .ci_pipelines
- .create!(source: :push, ref: 'master', sha: commit.sha, protected: false)
+ create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
end
it 'includes status as "created" and a last_pipeline object' do
@@ -1090,9 +1088,7 @@ RSpec.describe API::Commits do
shared_examples_for 'ref with unaccessible pipeline' do
let!(:pipeline) do
- project
- .ci_pipelines
- .create!(source: :push, ref: 'master', sha: commit.sha, protected: false)
+ create(:ci_empty_pipeline, project: project, status: :created, source: :push, ref: 'master', sha: commit.sha, protected: false)
end
it 'does not include last_pipeline' do
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 2d85d7b9583..1836233594d 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -574,6 +574,27 @@ RSpec.describe API::GenericPackages do
end
end
+ context 'with package status' do
+ where(:package_status, :expected_status) do
+ :default | :success
+ :hidden | :success
+ :error | :not_found
+ end
+
+ with_them do
+ before do
+ project.add_developer(user)
+ package.update!(status: package_status)
+ end
+
+ it "responds with #{params[:expected_status]}" do
+ download_file(personal_access_token_header)
+
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+ end
+
context 'event tracking' do
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index 8ede6e1538c..755585f8e0e 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'Query.ciConfig' do
ciConfig(projectPath: "#{project.full_path}", content: "#{content}", dryRun: false) {
status
errors
+ warnings
stages {
nodes {
name
@@ -73,6 +74,7 @@ RSpec.describe 'Query.ciConfig' do
expect(graphql_data['ciConfig']).to eq(
"status" => "VALID",
"errors" => [],
+ "warnings" => [],
"stages" =>
{
"nodes" =>
@@ -220,6 +222,21 @@ RSpec.describe 'Query.ciConfig' do
)
end
+ context 'when using deprecated keywords' do
+ let_it_be(:content) do
+ YAML.dump(
+ rspec: { script: 'ls' },
+ types: ['test']
+ )
+ end
+
+ it 'returns a warning' do
+ post_graphql_query
+
+ expect(graphql_data['ciConfig']['warnings']).to include('root `types` is deprecated in 9.0 and will be removed in 15.0.')
+ end
+ end
+
context 'when the config file includes other files' do
let_it_be(:content) do
YAML.dump(
@@ -250,6 +267,7 @@ RSpec.describe 'Query.ciConfig' do
expect(graphql_data['ciConfig']).to eq(
"status" => "VALID",
"errors" => [],
+ "warnings" => [],
"stages" =>
{
"nodes" =>
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 3a1df3525ef..b191b585d06 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -44,6 +44,10 @@ RSpec.describe 'Query.project.pipeline' do
name
jobs {
nodes {
+ downstreamPipeline {
+ id
+ path
+ }
name
needs {
nodes { #{all_graphql_fields_for('CiBuildNeed')} }
@@ -131,6 +135,8 @@ RSpec.describe 'Query.project.pipeline' do
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ create(:ci_bridge, name: 'bridge-1', pipeline: pipeline, downstream_pipeline: create(:ci_pipeline))
+
post_graphql(query, current_user: user)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
@@ -139,6 +145,8 @@ RSpec.describe 'Query.project.pipeline' do
create(:ci_build, name: 'test-a', pipeline: pipeline)
create(:ci_build, name: 'test-b', pipeline: pipeline)
+ create(:ci_bridge, name: 'bridge-2', pipeline: pipeline, downstream_pipeline: create(:ci_pipeline))
+ create(:ci_bridge, name: 'bridge-3', pipeline: pipeline, downstream_pipeline: create(:ci_pipeline))
expect do
post_graphql(query, current_user: user)
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 95ddd0250e7..5ae68be46a2 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -12,6 +12,38 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
travel_to(Time.current) { example.run }
end
+ describe 'sha' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ fullSha: sha
+ shortSha: sha(format: SHORT)
+ alsoFull: sha(format: LONG)
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all formats of the SHA' do
+ post_graphql(query, current_user: user)
+
+ expect(pipelines_graphql_data).to include(
+ 'fullSha' => eq(pipeline.sha),
+ 'alsoFull' => eq(pipeline.sha),
+ 'shortSha' => eq(pipeline.short_sha)
+ )
+ end
+ end
+
describe 'duration fields' do
let_it_be(:pipeline) do
create(:ci_pipeline, project: project)
@@ -251,6 +283,50 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
end
+ describe 'warningMessages' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:warning_message) { create(:ci_pipeline_message, pipeline: pipeline, content: 'warning') }
+
+ let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ warningMessages {
+ content
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns pipeline warnings' do
+ post_graphql(query, current_user: user)
+
+ expect(pipelines_graphql_data['warningMessages']).to contain_exactly(
+ a_hash_including('content' => 'warning')
+ )
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user)
+ end
+
+ pipeline_2 = create(:ci_pipeline, project: project)
+ create(:ci_pipeline_message, pipeline: pipeline_2, content: 'warning')
+
+ expect do
+ post_graphql(query, current_user: user)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
describe '.jobs(securityReportTypes)' do
let_it_be(:query) do
%(
@@ -420,4 +496,36 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
end
end
+
+ describe 'ref_path' do
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:pipeline_1) { create(:ci_pipeline, project: project, user: user, merge_request: merge_request) }
+ let_it_be(:pipeline_2) { create(:ci_pipeline, project: project, user: user, merge_request: merge_request) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ refPath
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user)
+ end
+
+ create(:ci_pipeline, project: project, user: user, merge_request: merge_request)
+
+ expect do
+ post_graphql(query, current_user: user)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 98d3a3b1c51..8c919b48849 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Query.runner(id)' do
let_it_be(:active_instance_runner) do
create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago,
active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true)
+ access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom)
end
let_it_be(:inactive_instance_runner) do
@@ -22,7 +22,7 @@ RSpec.describe 'Query.runner(id)' do
let_it_be(:active_group_runner) do
create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago,
active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600,
- access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true)
+ access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell)
end
def get_runner(id)
@@ -57,6 +57,7 @@ RSpec.describe 'Query.runner(id)' do
expect(runner_data).to match a_hash_including(
'id' => "gid://gitlab/Ci::Runner/#{runner.id}",
'description' => runner.description,
+ 'createdAt' => runner.created_at&.iso8601,
'contactedAt' => runner.contacted_at&.iso8601,
'version' => runner.version,
'shortSha' => runner.short_sha,
@@ -69,6 +70,7 @@ RSpec.describe 'Query.runner(id)' do
'runUntagged' => runner.run_untagged,
'ipAddress' => runner.ip_address,
'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE',
+ 'executorName' => runner.executor_type&.dasherize,
'jobCount' => 0,
'projectCount' => nil,
'adminUrl' => "http://localhost/admin/runners/#{runner.id}",
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 31cb0393d7f..06afb5b9a49 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -56,12 +56,16 @@ RSpec.describe 'getting group members information' do
context 'member relations' do
let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) }
+ let_it_be(:invited_group) { create(:group, :public) }
let_it_be(:child_user) { create(:user) }
let_it_be(:grandchild_user) { create(:user) }
+ let_it_be(:invited_user) { create(:user) }
+ let_it_be(:group_link) { create(:group_group_link, shared_group: child_group, shared_with_group: invited_group) }
before_all do
child_group.add_guest(child_user)
grandchild_group.add_guest(grandchild_user)
+ invited_group.add_guest(invited_user)
end
it 'returns direct members' do
@@ -71,6 +75,13 @@ RSpec.describe 'getting group members information' do
expect_array_response(child_user)
end
+ it 'returns invited members plus inherited members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(invited_user, user_1, user_2, child_user)
+ end
+
it 'returns direct and inherited members' do
fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb
new file mode 100644
index 00000000000..0667e09d1e9
--- /dev/null
+++ b/spec/requests/api/graphql/group/work_item_types_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting a list of work item types for a group' do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+
+ before_all do
+ group.add_developer(developer)
+ end
+
+ let(:current_user) { developer }
+
+ let(:fields) do
+ <<~GRAPHQL
+ workItemTypes{
+ nodes { id name iconName }
+ }
+ GRAPHQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group.full_path },
+ fields
+ )
+ end
+
+ context 'when user has access to the group' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all default work item types' do
+ expect(graphql_data.dig('group', 'workItemTypes', 'nodes')).to match_array(
+ WorkItems::Type.default.map do |type|
+ hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name)
+ end
+ )
+ end
+ end
+
+ context "when user doesn't have acces to the group" do
+ let(:current_user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return the group' do
+ expect(graphql_data).to eq('group' => nil)
+ end
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'makes the workItemTypes field unavailable' do
+ expect(graphql_errors).to contain_exactly(hash_including("message" => "Field 'workItemTypes' doesn't exist on type 'Group'"))
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
index 2da69509ad6..79d687a2bdb 100644
--- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
@@ -6,13 +6,18 @@ RSpec.describe 'Setting issues crm contacts' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
- let_it_be(:contacts) { create_list(:contact, 4, group: group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:subgroup) { create(:group, :crm_enabled, parent: group) }
+ let_it_be(:project) { create(:project, group: subgroup) }
+ let_it_be(:group_contacts) { create_list(:contact, 4, group: group) }
+ let_it_be(:subgroup_contacts) { create_list(:contact, 4, group: subgroup) }
let(:issue) { create(:issue, project: project) }
let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
- let(:contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] }
+ let(:contacts) { subgroup_contacts }
+ let(:initial_contacts) { contacts[0..1] }
+ let(:mutation_contacts) { contacts[1..2] }
+ let(:contact_ids) { contact_global_ids(mutation_contacts) }
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
let(:mutation) do
@@ -42,9 +47,47 @@ RSpec.describe 'Setting issues crm contacts' do
graphql_mutation_response(:issue_set_crm_contacts)
end
+ def contact_global_ids(contacts)
+ contacts.map { |contact| global_id_of(contact) }
+ end
+
before do
- create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
- create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
+ initial_contacts.each { |contact| create(:issue_customer_relations_contact, issue: issue, contact: contact) }
+ end
+
+ shared_examples 'successful mutation' do
+ context 'replace' do
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array(contact_global_ids(mutation_contacts))
+ end
+ end
+
+ context 'append' do
+ let(:mutation_contacts) { [contacts[3]] }
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
+
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array(contact_global_ids(initial_contacts + mutation_contacts))
+ end
+ end
+
+ context 'remove' do
+ let(:mutation_contacts) { [contacts[0]] }
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
+
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array(contact_global_ids(initial_contacts - mutation_contacts))
+ end
+ end
end
context 'when the user has no permission' do
@@ -73,37 +116,14 @@ RSpec.describe 'Setting issues crm contacts' do
end
end
- context 'replace' do
- it 'updates the issue with correct contacts' do
- post_graphql_mutation(mutation, current_user: user)
-
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])])
- end
- end
+ context 'with issue group contacts' do
+ let(:contacts) { subgroup_contacts }
- context 'append' do
- let(:contact_ids) { [global_id_of(contacts[3])] }
- let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
-
- it 'updates the issue with correct contacts' do
- post_graphql_mutation(mutation, current_user: user)
-
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])])
- end
+ it_behaves_like 'successful mutation'
end
- context 'remove' do
- let(:contact_ids) { [global_id_of(contacts[0])] }
- let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
-
- it 'updates the issue with correct contacts' do
- post_graphql_mutation(mutation, current_user: user)
-
- expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
- .to match_array([global_id_of(contacts[1])])
- end
+ context 'with issue ancestor group contacts' do
+ it_behaves_like 'successful mutation'
end
context 'when the contact does not exist' do
@@ -118,7 +138,7 @@ RSpec.describe 'Setting issues crm contacts' do
end
context 'when the contact belongs to a different group' do
- let(:group2) { create(:group) }
+ let(:group2) { create(:group, :crm_enabled) }
let(:contact) { create(:contact, group: group2) }
let(:contact_ids) { [global_id_of(contact)] }
@@ -158,4 +178,17 @@ RSpec.describe 'Setting issues crm contacts' do
end
end
end
+
+ context 'when crm_enabled is false' do
+ let(:issue) { create(:issue) }
+ let(:initial_contacts) { [] }
+
+ it 'raises expected error' do
+ issue.project.add_reporter(user)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..0166871502b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting the escalation status of an incident' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:incident, project: project) }
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+ let_it_be(:user) { create(:user) }
+
+ let(:status) { 'ACKNOWLEDGED' }
+ let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, status: status } }
+
+ let(:current_user) { user }
+ let(:mutation) do
+ graphql_mutation(:issue_set_escalation_status, input) do
+ <<~QL
+ clientMutationId
+ errors
+ issue {
+ iid
+ escalationStatus
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_status) }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ context 'when user does not have permission to edit the escalation status' do
+ let(:current_user) { create(:user) }
+
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ it 'sets given escalation_policy to the escalation status for the issue' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['issue']['escalationStatus']).to eq(status)
+ expect(escalation_status.reload.status_name).to eq(:acknowledged)
+ end
+
+ context 'when status argument is not given' do
+ let(:input) { {} }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { contain_exactly(include('status (Expected value to not be null)')) }
+ end
+ end
+
+ context 'when status argument is invalid' do
+ let(:status) { 'INVALID' }
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :status
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
new file mode 100644
index 00000000000..e7a0c7753fb
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a work item' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+
+ let(:input) do
+ {
+ 'title' => 'new title',
+ 'description' => 'new description',
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) }
+
+ let(:mutation_response) { graphql_mutation_response(:work_item_create) }
+
+ context 'the user is not allowed to create a work item' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to create a work item' do
+ let(:current_user) { developer }
+
+ it 'creates the work item' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(1)
+
+ created_work_item = WorkItem.last
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(created_work_item.issue_type).to eq('task')
+ expect(created_work_item.work_item_type.base_type).to eq('task')
+ expect(mutation_response['workItem']).to include(
+ input.except('workItemTypeId').merge(
+ 'id' => created_work_item.to_global_id.to_s,
+ 'workItemType' => hash_including('name' => 'Task')
+ )
+ )
+ end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::WorkItems::Create }
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index a9019a7611a..2ff3bc7cc47 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe 'package details' do
include GraphqlHelpers
- let_it_be_with_reload(:project) { create(:project) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
@@ -17,7 +19,6 @@ RSpec.describe 'package details' do
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
let(:package_files) {all_graphql_fields_for('PackageFile')}
- let(:user) { project.owner }
let(:package_global_id) { global_id_of(composer_package) }
let(:package_details) { graphql_data_at(:package) }
@@ -37,145 +38,198 @@ RSpec.describe 'package details' do
subject { post_graphql(query, current_user: user) }
- it_behaves_like 'a working graphql query' do
+ context 'with unauthorized user' do
before do
- subject
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
- it 'matches the JSON schema' do
- expect(package_details).to match_schema('graphql/packages/package_details')
+ it 'returns no packages' do
+ subject
+
+ expect(graphql_data_at(:package)).to be_nil
end
end
- context 'there are other versions of this package' do
- let(:depth) { 3 }
- let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
-
- let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
+ context 'with authorized user' do
+ before do
+ project.add_developer(user)
+ end
- it 'includes the sibling versions' do
- subject
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
- expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
- siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
- )
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
end
- context 'going deeper' do
- let(:depth) { 6 }
+ context 'there are other versions of this package' do
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
- it 'does not create a cycle of versions' do
+ let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
+
+ it 'includes the sibling versions' do
subject
- expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
- expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil]
+ expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
+ siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
+ )
end
- end
- end
- context 'with a batched query' do
- let_it_be(:conan_package) { create(:conan_package, project: project) }
+ context 'going deeper' do
+ let(:depth) { 6 }
- let(:batch_query) do
- <<~QUERY
- {
- a: package(id: "#{global_id_of(composer_package)}") { name }
- b: package(id: "#{global_id_of(conan_package)}") { name }
- }
- QUERY
+ it 'does not create a cycle of versions' do
+ subject
+
+ expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
+ expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to match_array [nil, nil]
+ end
+ end
end
- let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
+ context 'with package files pending destruction' do
+ let_it_be(:package_file) { create(:package_file, package: composer_package) }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
- it 'returns an error for the second package and data for the first' do
- post_graphql(batch_query, current_user: user)
+ let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } }
- expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
+ it 'does not return them' do
+ subject
- expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
- expect(graphql_data_at(:b)).to be(nil)
- end
- end
+ expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
+ end
- context 'with unauthorized user' do
- let_it_be(:user) { create(:user) }
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ it 'returns them' do
+ subject
+
+ expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
+ end
+ end
end
- it 'returns no packages' do
- subject
+ context 'with a batched query' do
+ let_it_be(:conan_package) { create(:conan_package, project: project) }
- expect(graphql_data_at(:package)).to be_nil
- end
- end
+ let(:batch_query) do
+ <<~QUERY
+ {
+ a: package(id: "#{global_id_of(composer_package)}") { name }
+ b: package(id: "#{global_id_of(conan_package)}") { name }
+ }
+ QUERY
+ end
- context 'pipelines field', :aggregate_failures do
- let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
- let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
+ let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
- before do
- composer_package.pipelines = pipelines
- composer_package.save!
- end
+ it 'returns an error for the second package and data for the first' do
+ post_graphql(batch_query, current_user: user)
- def run_query(args)
- pipelines_nodes = <<~QUERY
- nodes {
- id
- }
- pageInfo {
- startCursor
- endCursor
- }
- QUERY
+ expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
- query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
- post_graphql(query, current_user: user)
+ expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
+ expect(graphql_data_at(:b)).to be(nil)
+ end
end
- it 'loads the second page with pagination first correctly' do
- run_query(first: 2)
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ context 'pipelines field', :aggregate_failures do
+ let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
+ let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
- expect(pipeline_ids).to eq(pipeline_gids[0..1])
+ before do
+ composer_package.pipelines = pipelines
+ composer_package.save!
+ end
- cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
+ def run_query(args)
+ pipelines_nodes = <<~QUERY
+ nodes {
+ id
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ }
+ QUERY
+
+ query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
+ post_graphql(query, current_user: user)
+ end
- run_query(first: 2, after: cursor)
+ it 'loads the second page with pagination first correctly' do
+ run_query(first: 2)
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ expect(pipeline_ids).to eq(pipeline_gids[0..1])
- expect(pipeline_ids).to eq(pipeline_gids[2..3])
- end
+ cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
- it 'loads the second page with pagination last correctly' do
- run_query(last: 2)
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ run_query(first: 2, after: cursor)
- expect(pipeline_ids).to eq(pipeline_gids[4..5])
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
+ expect(pipeline_ids).to eq(pipeline_gids[2..3])
+ end
- run_query(last: 2, before: cursor)
+ it 'loads the second page with pagination last correctly' do
+ run_query(last: 2)
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
+ expect(pipeline_ids).to eq(pipeline_gids[4..5])
- expect(pipeline_ids).to eq(pipeline_gids[2..3])
- end
+ cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
+
+ run_query(last: 2, before: cursor)
+
+ pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
- context 'with unauthorized user' do
- let_it_be(:user) { create(:user) }
+ expect(pipeline_ids).to eq(pipeline_gids[2..3])
+ end
+ end
+ context 'package managers paths' do
before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ subject
end
- it 'returns no packages' do
- run_query(first: 2)
+ it 'returns npm_url correctly' do
+ expect(graphql_data_at(:package, :npm_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/npm")
+ end
+
+ it 'returns maven_url correctly' do
+ expect(graphql_data_at(:package, :maven_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/maven")
+ end
+
+ it 'returns conan_url correctly' do
+ expect(graphql_data_at(:package, :conan_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/conan")
+ end
+
+ it 'returns nuget_url correctly' do
+ expect(graphql_data_at(:package, :nuget_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/nuget/index.json")
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
+ end
+
+ it 'returns pypi_setup_url correctly' do
+ expect(graphql_data_at(:package, :pypi_setup_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/pypi")
+ end
+
+ it 'returns composer_url correctly' do
+ expect(graphql_data_at(:package, :composer_url)).to eq("http://localhost/api/v4/group/#{group.id}/-/packages/composer/packages.json")
+ end
- expect(graphql_data_at(:package)).to be_nil
+ it 'returns composer_config_repository_url correctly' do
+ expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
end
end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index b3e91afb5b3..f358ec3e53f 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'when fetching escalation status' do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
+
+ let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ id
+ escalationStatus
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issue_a.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it 'returns the escalation status values' do
+ post_graphql(query, current_user: current_user)
+
+ statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') }
+
+ expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
+
+ new_incident = create(:incident, project: project)
+ create(:incident_management_issuable_escalation_status, issue: new_incident)
+
+ expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count)
+ end
+ end
+
describe 'N+1 query checks' do
let(:extra_iid_for_second_query) { issue_b.iid.to_s }
let(:search_params) { { iids: [issue_a.iid.to_s] } }
diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb
new file mode 100644
index 00000000000..2caaedda2a1
--- /dev/null
+++ b/spec/requests/api/graphql/project/work_item_types_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting a list of work item types for a project' do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_developer(developer)
+ end
+
+ let(:current_user) { developer }
+
+ let(:fields) do
+ <<~GRAPHQL
+ workItemTypes{
+ nodes { id name iconName }
+ }
+ GRAPHQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ fields
+ )
+ end
+
+ context 'when user has access to the project' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all default work item types' do
+ expect(graphql_data.dig('project', 'workItemTypes', 'nodes')).to match_array(
+ WorkItems::Type.default.map do |type|
+ hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name)
+ end
+ )
+ end
+ end
+
+ context "when user doesn't have access to the project" do
+ let(:current_user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return the project' do
+ expect(graphql_data).to eq('project' => nil)
+ end
+ end
+
+ context 'when the work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'makes the workItemTypes field unavailable' do
+ expect(graphql_errors).to contain_exactly(hash_including("message" => "Field 'workItemTypes' doesn't exist on type 'Project'"))
+ end
+ end
+end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index d226bb07c73..88c004345fc 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -801,6 +801,54 @@ RSpec.describe API::Groups do
expect(json_response['shared_projects'].count).to eq(limit)
end
end
+
+ context 'when a group is shared', :aggregate_failures do
+ let_it_be(:shared_group) { create(:group) }
+ let_it_be(:group2_sub) { create(:group, :private, parent: group2) }
+ let_it_be(:group_link_1) { create(:group_group_link, shared_group: shared_group, shared_with_group: group1) }
+ let_it_be(:group_link_2) { create(:group_group_link, shared_group: shared_group, shared_with_group: group2_sub) }
+
+ subject(:shared_with_groups) { json_response['shared_with_groups'].map { _1['group_id']} }
+
+ context 'when authenticated as admin' do
+ it 'returns all groups that share the group' do
+ get api("/groups/#{shared_group.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns only public groups that share the group' do
+ get api("/groups/#{shared_group.id}")
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id)
+ end
+ end
+
+ context 'when authenticated as a member of a parent group that has shared the group' do
+ it 'returns private group if direct member' do
+ group2_sub.add_guest(user3)
+
+ get api("/groups/#{shared_group.id}", user3)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
+ end
+
+ it 'returns private group if inherited member' do
+ inherited_guest_member = create(:user)
+ group2.add_guest(inherited_guest_member)
+
+ get api("/groups/#{shared_group.id}", inherited_guest_member)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
+ end
+ end
+ end
end
describe 'PUT /groups/:id' do
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index 649647804c0..033c80a5696 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -55,8 +55,10 @@ RSpec.describe API::Integrations do
current_integration = project.integrations.first
events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
query_strings = []
- events.each do |event|
- query_strings << "#{event}=#{!current_integration[event]}"
+ events.map(&:to_sym).each do |event|
+ event_value = !current_integration[event]
+ query_strings << "#{event}=#{event_value}"
+ integration_attrs[event] = event_value if integration_attrs[event].present?
end
query_strings = query_strings.join('&')
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 0a71eb43f81..9aa8aaafc68 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -372,7 +372,38 @@ RSpec.describe API::Internal::Base do
end
end
- describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do
+ describe "POST /internal/allowed", :clean_gitlab_redis_shared_state, :clean_gitlab_redis_rate_limiting do
+ shared_examples 'rate limited request' do
+ let(:action) { 'git-upload-pack' }
+ let(:actor) { key }
+
+ it 'is throttled by rate limiter' do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(1)
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:gitlab_shell_operation, scope: [action, project.full_path, actor]).twice.and_call_original
+
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ request
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
+ end
+
+ context 'when rate_limit_gitlab_shell feature flag is disabled' do
+ before do
+ stub_feature_flags(rate_limit_gitlab_shell: false)
+ end
+
+ it 'is not throttled by rate limiter' do
+ expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ subject
+ end
+ end
+ end
+
context "access granted" do
let(:env) { {} }
@@ -530,6 +561,32 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today)
end
+
+ it_behaves_like 'rate limited request' do
+ def request
+ pull(key, project)
+ end
+ end
+
+ context 'when user_id is passed' do
+ it_behaves_like 'rate limited request' do
+ let(:actor) { user }
+
+ def request
+ post(
+ api("/internal/allowed"),
+ params: {
+ user_id: user.id,
+ project: full_path_for(project),
+ gl_repository: gl_repository_for(project),
+ action: 'git-upload-pack',
+ secret_token: secret_token,
+ protocol: 'ssh'
+ }
+ )
+ end
+ end
+ end
end
context "with a feature flag enabled for a project" do
@@ -576,6 +633,14 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
expect(user.reload.last_activity_on).to be_nil
end
+
+ it_behaves_like 'rate limited request' do
+ let(:action) { 'git-receive-pack' }
+
+ def request
+ push(key, project)
+ end
+ end
end
context 'when receive_max_input_size has been updated' do
@@ -838,6 +903,14 @@ RSpec.describe API::Internal::Base do
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
end
+
+ it_behaves_like 'rate limited request' do
+ let(:action) { 'git-upload-archive' }
+
+ def request
+ archive(key, project)
+ end
+ end
end
context "not added to project" do
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 245e4e6ba15..59d185fe6c8 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -53,7 +53,9 @@ RSpec.describe API::Internal::Kubernetes do
shared_examples 'agent token tracking' do
it 'tracks token usage' do
- expect { response }.to change { agent_token.reload.read_attribute(:last_used_at) }
+ expect do
+ send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+ end.to change { agent_token.reload.read_attribute(:last_used_at) }
end
end
@@ -149,7 +151,7 @@ RSpec.describe API::Internal::Kubernetes do
let(:agent) { agent_token.agent }
let(:project) { agent.project }
- shared_examples 'agent token tracking'
+ include_examples 'agent token tracking'
it 'returns expected data', :aggregate_failures do
send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
diff --git a/spec/requests/api/internal/mail_room_spec.rb b/spec/requests/api/internal/mail_room_spec.rb
new file mode 100644
index 00000000000..f3ca3708c0c
--- /dev/null
+++ b/spec/requests/api/internal/mail_room_spec.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Internal::MailRoom do
+ let(:base_configs) do
+ {
+ enabled: true,
+ address: 'address@example.com',
+ port: 143,
+ ssl: false,
+ start_tls: false,
+ mailbox: 'inbox',
+ idle_timeout: 60,
+ log_path: Rails.root.join('log', 'mail_room_json.log').to_s,
+ expunge_deleted: false
+ }
+ end
+
+ let(:enabled_configs) do
+ {
+ incoming_email: base_configs.merge(
+ secure_file: Rails.root.join('tmp', 'tests', '.incoming_email_secret').to_s
+ ),
+ service_desk_email: base_configs.merge(
+ secure_file: Rails.root.join('tmp', 'tests', '.service_desk_email').to_s
+ )
+ }
+ end
+
+ let(:auth_payload) { { 'iss' => Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_JWT_ISSUER, 'iat' => (Time.now - 10.seconds).to_i } }
+
+ let(:incoming_email_secret) { 'incoming_email_secret' }
+ let(:service_desk_email_secret) { 'service_desk_email_secret' }
+
+ let(:email_content) { fixture_file("emails/commands_in_reply.eml") }
+
+ before do
+ allow(Gitlab::MailRoom::Authenticator).to receive(:secret).with(:incoming_email).and_return(incoming_email_secret)
+ allow(Gitlab::MailRoom::Authenticator).to receive(:secret).with(:service_desk_email).and_return(service_desk_email_secret)
+ allow(Gitlab::MailRoom).to receive(:enabled_configs).and_return(enabled_configs)
+ end
+
+ around do |example|
+ freeze_time do
+ example.run
+ end
+ end
+
+ describe "POST /internal/mail_room/*mailbox_type" do
+ context 'handle incoming_email successfully' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, incoming_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'schedules a EmailReceiverWorker job with raw email content' do
+ Sidekiq::Testing.fake! do
+ expect do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers, params: email_content
+ end.to change { EmailReceiverWorker.jobs.size }.by(1)
+ end
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ job = EmailReceiverWorker.jobs.last
+ expect(job).to match a_hash_including('args' => [email_content])
+ end
+ end
+
+ context 'handle service_desk_email successfully' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, service_desk_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'schedules a ServiceDeskEmailReceiverWorker job with raw email content' do
+ Sidekiq::Testing.fake! do
+ expect do
+ post api("/internal/mail_room/service_desk_email"), headers: auth_headers, params: email_content
+ end.to change { ServiceDeskEmailReceiverWorker.jobs.size }.by(1)
+ end
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ job = ServiceDeskEmailReceiverWorker.jobs.last
+ expect(job).to match a_hash_including('args' => [email_content])
+ end
+ end
+
+ context 'email content exceeds limit' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, incoming_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ before do
+ allow(EmailReceiverWorker).to receive(:perform_async).and_raise(
+ Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(EmailReceiverWorker, email_content.bytesize, email_content.bytesize - 1)
+ )
+ end
+
+ it 'responds with 400 bad request and replies with a failure message' do
+ perform_enqueued_jobs do
+ Sidekiq::Testing.fake! do
+ expect do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers, params: email_content
+ end.not_to change { EmailReceiverWorker.jobs.size }
+ end
+ end
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::Json.parse(response.body)).to match a_hash_including(
+ "success" => false,
+ "message" => "We couldn't process your email because it is too large. Please create your issue or comment through the web interface."
+ )
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to match_array(["jake@adventuretime.ooo"])
+ expect(email.subject).to include("Rejected")
+ expect(email.body.parts.last.to_s).to include("We couldn't process your email")
+ end
+ end
+
+ context 'not authenticated' do
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/incoming_email")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'wrong token authentication' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, 'wrongsecret', 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'wrong mailbox type authentication' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, service_desk_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/incoming_email"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'not supported mailbox type' do
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, incoming_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/invalid_mailbox_type"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'not enabled mailbox type' do
+ let(:enabled_configs) do
+ {
+ incoming_email: base_configs.merge(
+ secure_file: Rails.root.join('tmp', 'tests', '.incoming_email_secret').to_s
+ )
+ }
+ end
+
+ let(:auth_headers) do
+ jwt_token = JWT.encode(auth_payload, service_desk_email_secret, 'HS256')
+ { Gitlab::MailRoom::Authenticator::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ it 'responds with 401 Unauthorized' do
+ post api("/internal/mail_room/service_desk_email"), headers: auth_headers
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 0e83b964121..7c1e731a99a 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -121,8 +121,8 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['status']).to eq('valid')
- expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq([])
+ expect(json_response['warnings']).to match_array([])
+ expect(json_response['errors']).to match_array([])
end
it 'outputs expanded yaml content' do
@@ -149,7 +149,20 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['status']).to eq('valid')
expect(json_response['warnings']).not_to be_empty
- expect(json_response['errors']).to eq([])
+ expect(json_response['errors']).to match_array([])
+ end
+ end
+
+ context 'with valid .gitlab-ci.yaml using deprecated keywords' do
+ let(:yaml_content) { { job: { script: 'ls' }, types: ['test'] }.to_yaml }
+
+ it 'passes validation but returns warnings' do
+ post api('/ci/lint', api_user), params: { content: yaml_content }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['warnings']).not_to be_empty
+ expect(json_response['errors']).to match_array([])
end
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 5a682ee8532..bc325aad823 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -425,7 +425,7 @@ RSpec.describe API::MavenPackages do
context 'internal project' do
before do
- group.group_member(user).destroy!
+ group.member(user).destroy!
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 7c147419354..a751f785913 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1269,6 +1269,7 @@ RSpec.describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response).to include('merged_by',
+ 'merge_user',
'merged_at',
'closed_by',
'closed_at',
@@ -1279,9 +1280,10 @@ RSpec.describe API::MergeRequests do
end
it 'returns correct values' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.reload.iid}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response['merged_by']['id']).to eq(merge_request.metrics.merged_by_id)
+ expect(json_response['merge_user']['id']).to eq(merge_request.metrics.merged_by_id)
expect(Time.parse(json_response['merged_at'])).to be_like_time(merge_request.metrics.merged_at)
expect(json_response['closed_by']['id']).to eq(merge_request.metrics.latest_closed_by_id)
expect(Time.parse(json_response['closed_at'])).to be_like_time(merge_request.metrics.latest_closed_at)
@@ -1292,6 +1294,32 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'merge_user' do
+ context 'when MR is set to MWPS' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, source_project: project, target_project: project) }
+
+ it 'returns user who set MWPS' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['merge_user']['id']).to eq(user.id)
+ end
+
+ context 'when MR is already merged' do
+ before do
+ merge_request.metrics.update!(merged_by: user2)
+ end
+
+ it 'returns user who actually merged' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['merge_user']['id']).to eq(user2.id)
+ end
+ end
+ end
+ end
+
context 'head_pipeline' do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, source_branch: 'markdown', title: "Test") }
@@ -3278,9 +3306,10 @@ RSpec.describe API::MergeRequests do
context 'when skip_ci parameter is set' do
it 'enqueues a rebase of the merge request with skip_ci flag set' do
- allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
+ with_status = RebaseWorker.with_status
- expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original
+ expect(RebaseWorker).to receive(:with_status).and_return(with_status)
+ expect(with_status).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original
Sidekiq::Testing.fake! do
expect do
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index eb1f04d193e..7a6b1599154 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -76,6 +76,30 @@ RSpec.describe API::PackageFiles do
end
end
end
+
+ context 'with package files pending destruction' do
+ let!(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ let(:package_file_ids) { json_response.map { |e| e['id'] } }
+
+ it 'does not return them' do
+ get api(url, user)
+
+ expect(package_file_ids).not_to include(package_file_pending_destruction.id)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ get api(url, user)
+
+ expect(package_file_ids).to include(package_file_pending_destruction.id)
+ end
+ end
+ end
end
end
@@ -149,6 +173,32 @@ RSpec.describe API::PackageFiles do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with package file pending destruction' do
+ let!(:package_file_id) { create(:package_file, :pending_destruction, package: package).id }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'can not be accessed', :aggregate_failures do
+ expect { api_request }.not_to change { package.package_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'can be accessed', :aggregate_failures do
+ expect { api_request }.to change { package.package_files.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8406ded85d8..bf41a808219 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3704,6 +3704,46 @@ RSpec.describe API::Projects do
expect { subject }.to change { project.reload.keep_latest_artifact }.to(true)
end
end
+
+ context 'attribute mr_default_target_self' do
+ let_it_be(:source_project) { create(:project, :public) }
+
+ let(:forked_project) { fork_project(source_project, user) }
+
+ it 'is by default set to false' do
+ expect(source_project.mr_default_target_self).to be false
+ expect(forked_project.mr_default_target_self).to be false
+ end
+
+ describe 'for a non-forked project' do
+ before_all do
+ source_project.add_maintainer(user)
+ end
+
+ it 'is not exposed' do
+ get api("/projects/#{source_project.id}", user)
+
+ expect(json_response).not_to include('mr_default_target_self')
+ end
+
+ it 'is not possible to update' do
+ put api("/projects/#{source_project.id}", user), params: { mr_default_target_self: true }
+
+ source_project.reload
+ expect(source_project.mr_default_target_self).to be false
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ describe 'for a forked project' do
+ it 'updates to true' do
+ put api("/projects/#{forked_project.id}", user), params: { mr_default_target_self: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['mr_default_target_self']).to eq(true)
+ end
+ end
+ end
end
describe 'POST /projects/:id/archive' do
@@ -4213,7 +4253,13 @@ RSpec.describe API::Projects do
end
it 'accepts custom parameters for the target project' do
- post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project', description: 'A description', visibility: 'private' }
+ post api("/projects/#{project.id}/fork", user2),
+ params: {
+ name: 'My Random Project',
+ description: 'A description',
+ visibility: 'private',
+ mr_default_target_self: true
+ }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('My Random Project')
@@ -4224,6 +4270,7 @@ RSpec.describe API::Projects do
expect(json_response['description']).to eq('A description')
expect(json_response['visibility']).to eq('private')
expect(json_response['import_status']).to eq('scheduled')
+ expect(json_response['mr_default_target_self']).to eq(true)
expect(json_response).to include("import_error")
end
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 23061ab4bf0..7e3e682767f 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -3,25 +3,27 @@
require "spec_helper"
RSpec.describe API::ResourceAccessTokens do
- context "when the resource is a project" do
- let_it_be(:project) { create(:project) }
- let_it_be(:other_project) { create(:project) }
- let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_non_priviledged) { create(:user) }
- describe "GET projects/:id/access_tokens" do
- subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) }
+ shared_examples 'resource access token API' do |source_type|
+ context "GET #{source_type}s/:id/access_tokens" do
+ subject(:get_tokens) { get api("/#{source_type}s/#{resource_id}/access_tokens", user) }
- context "when the user has maintainer permissions" do
+ context "when the user has valid permissions" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
before do
- project.add_maintainer(user)
- project.add_maintainer(project_bot)
+ if source_type == 'project'
+ resource.add_maintainer(project_bot)
+ else
+ resource.add_owner(project_bot)
+ end
end
- it "gets a list of access tokens for the specified project" do
+ it "gets a list of access tokens for the specified #{source_type}" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
@@ -38,16 +40,22 @@ RSpec.describe API::ResourceAccessTokens do
expect(api_get_token["name"]).to eq(token.name)
expect(api_get_token["scopes"]).to eq(token.scopes)
- expect(api_get_token["access_level"]).to eq(project.team.max_member_access(token.user.id))
+
+ if source_type == 'project'
+ expect(api_get_token["access_level"]).to eq(resource.team.max_member_access(token.user.id))
+ else
+ expect(api_get_token["access_level"]).to eq(resource.max_member_access_for_user(token.user))
+ end
+
expect(api_get_token["expires_at"]).to eq(token.expires_at.to_date.iso8601)
expect(api_get_token).not_to have_key('token')
end
- context "when using a project access token to GET other project access tokens" do
+ context "when using a #{source_type} access token to GET other #{source_type} access tokens" do
let_it_be(:token) { access_tokens.first }
- it "gets a list of access tokens for the specified project" do
- get api("/projects/#{project_id}/access_tokens", personal_access_token: token)
+ it "gets a list of access tokens for the specified #{source_type}" do
+ get api("/#{source_type}s/#{resource_id}/access_tokens", personal_access_token: token)
token_ids = json_response.map { |token| token['id'] }
@@ -56,16 +64,15 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when tokens belong to a different project" do
+ context "when tokens belong to a different #{source_type}" do
let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: bot) }
before do
- other_project.add_maintainer(bot)
- other_project.add_maintainer(user)
+ other_resource.add_maintainer(bot)
end
- it "does not return tokens from a different project" do
+ it "does not return tokens from a different #{source_type}" do
get_tokens
token_ids = json_response.map { |token| token['id'] }
@@ -74,12 +81,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when the project has no access tokens" do
- let(:project_id) { other_project.id }
-
- before do
- other_project.add_maintainer(user)
- end
+ context "when the #{source_type} has no access tokens" do
+ let(:resource_id) { other_resource.id }
it 'returns an empty array' do
get_tokens
@@ -89,8 +92,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when trying to get the tokens of a different project" do
- let_it_be(:project_id) { other_project.id }
+ context "when trying to get the tokens of a different #{source_type}" do
+ let_it_be(:resource_id) { unknown_resource.id }
it "returns 404" do
get_tokens
@@ -99,8 +102,8 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when the project does not exist" do
- let(:project_id) { non_existing_record_id }
+ context "when the #{source_type} does not exist" do
+ let(:resource_id) { non_existing_record_id }
it "returns 404" do
get_tokens
@@ -111,13 +114,13 @@ RSpec.describe API::ResourceAccessTokens do
end
context "when the user does not have valid permissions" do
+ let_it_be(:user) { user_non_priviledged }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
before do
- project.add_developer(user)
- project.add_maintainer(project_bot)
+ resource.add_maintainer(project_bot)
end
it "returns 401" do
@@ -128,40 +131,36 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do
- subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) }
+ context "DELETE #{source_type}s/:id/access_tokens/:token_id", :sidekiq_inline do
+ subject(:delete_token) { delete api("/#{source_type}s/#{resource_id}/access_tokens/#{token_id}", user) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
let_it_be(:token_id) { token.id }
before do
- project.add_maintainer(project_bot)
+ resource.add_maintainer(project_bot)
end
- context "when the user has maintainer permissions" do
- before do
- project.add_maintainer(user)
- end
-
- it "deletes the project access token from the project" do
+ context "when the user has valid permissions" do
+ it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
expect(User.exists?(project_bot.id)).to be_falsy
end
- context "when using project access token to DELETE other project access token" do
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
let_it_be(:other_project_bot) { create(:user, :project_bot) }
let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
let_it_be(:token_id) { other_token.id }
before do
- project.add_maintainer(other_project_bot)
+ resource.add_maintainer(other_project_bot)
end
- it "deletes the project access token from the project" do
+ it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
@@ -169,37 +168,31 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when attempting to delete a non-existent project access token" do
+ context "when attempting to delete a non-existent #{source_type} access token" do
let_it_be(:token_id) { non_existing_record_id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
+ expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}")
end
end
- context "when attempting to delete a token that does not belong to the specified project" do
- let_it_be(:project_id) { other_project.id }
-
- before do
- other_project.add_maintainer(user)
- end
+ context "when attempting to delete a token that does not belong to the specified #{source_type}" do
+ let_it_be(:resource_id) { other_resource.id }
it "does not delete the token, and returns 404" do
delete_token
expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
+ expect(response.body).to include("Could not find #{source_type} access token with token_id: #{token_id}")
end
end
end
context "when the user does not have valid permissions" do
- before do
- project.add_developer(user)
- end
+ let_it_be(:user) { user_non_priviledged }
it "does not delete the token, and returns 400", :aggregate_failures do
delete_token
@@ -211,23 +204,19 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- describe "POST projects/:id/access_tokens" do
+ context "POST #{source_type}s/:id/access_tokens" do
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at, access_level: access_level } }
let(:expires_at) { 1.month.from_now }
let(:access_level) { 20 }
- subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
+ subject(:create_token) { post api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params }
- context "when the user has maintainer permissions" do
- let_it_be(:project_id) { project.id }
-
- before do
- project.add_maintainer(user)
- end
+ context "when the user has valid permissions" do
+ let_it_be(:resource_id) { resource.id }
context "with valid params" do
context "with full params" do
- it "creates a project access token with the params", :aggregate_failures do
+ it "creates a #{source_type} access token with the params", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@@ -242,7 +231,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'expires_at' is not set" do
let(:expires_at) { nil }
- it "creates a project access token with the params", :aggregate_failures do
+ it "creates a #{source_type} access token with the params", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@@ -255,7 +244,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when 'access_level' is not set" do
let(:access_level) { nil }
- it 'creates a project access token with the default access level', :aggregate_failures do
+ it "creates a #{source_type} access token with the default access level", :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
@@ -272,7 +261,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'name' param" do
let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } }
- it "does not create a project access token without 'name'" do
+ it "does not create a #{source_type} access token without 'name'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
@@ -283,7 +272,7 @@ RSpec.describe API::ResourceAccessTokens do
context "when missing the 'scopes' param" do
let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } }
- it "does not create a project access token without 'scopes'" do
+ it "does not create a #{source_type} access token without 'scopes'" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
@@ -292,50 +281,80 @@ RSpec.describe API::ResourceAccessTokens do
end
end
- context "when trying to create a token in a different project" do
- let_it_be(:project_id) { other_project.id }
+ context "when trying to create a token in a different #{source_type}" do
+ let_it_be(:resource_id) { unknown_resource.id }
- it "does not create the token, and returns the project not found error" do
+ it "does not create the token, and returns the #{source_type} not found error" do
create_token
expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to include("Project Not Found")
+ expect(response.body).to include("#{source_type.capitalize} Not Found")
end
end
end
context "when the user does not have valid permissions" do
- let_it_be(:project_id) { project.id }
+ let_it_be(:resource_id) { resource.id }
- context "when the user is a developer" do
- before do
- project.add_developer(user)
- end
+ context "when the user role is too low" do
+ let_it_be(:user) { user_non_priviledged }
it "does not create the token, and returns the permission error" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include("User does not have permission to create project access token")
+ expect(response.body).to include("User does not have permission to create #{source_type} access token")
end
end
- context "when a project access token tries to create another project access token" do
+ context "when a #{source_type} access token tries to create another #{source_type} access token" do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { project_bot }
before do
- project.add_maintainer(user)
+ if source_type == 'project'
+ resource.add_maintainer(project_bot)
+ else
+ resource.add_owner(project_bot)
+ end
end
- it "does not allow a project access token to create another project access token" do
+ it "does not allow a #{source_type} access token to create another #{source_type} access token" do
create_token
expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include("User does not have permission to create project access token")
+ expect(response.body).to include("User does not have permission to create #{source_type} access token")
end
end
end
end
end
+
+ context 'when the resource is a project' do
+ let_it_be(:resource) { create(:project) }
+ let_it_be(:other_resource) { create(:project) }
+ let_it_be(:unknown_resource) { create(:project) }
+
+ before_all do
+ resource.add_maintainer(user)
+ other_resource.add_maintainer(user)
+ resource.add_developer(user_non_priviledged)
+ end
+
+ it_behaves_like 'resource access token API', 'project'
+ end
+
+ context 'when the resource is a group' do
+ let_it_be(:resource) { create(:group) }
+ let_it_be(:other_resource) { create(:group) }
+ let_it_be(:unknown_resource) { create(:project) }
+
+ before_all do
+ resource.add_owner(user)
+ other_resource.add_owner(user)
+ resource.add_maintainer(user_non_priviledged)
+ end
+
+ it_behaves_like 'resource access token API', 'group'
+ end
end
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index 9b104520b52..0e63a7269e7 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -173,6 +173,34 @@ RSpec.describe API::RubygemPackages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package, file_name: file_name) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'does not return them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(package_file_pending_destruction.file.file.read)
+ end
+ end
+ end
end
describe 'POST /api/v4/projects/:project_id/packages/rubygems/api/v1/gems/authorize' do
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index b75fe11b06d..24cd95781c3 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -346,6 +346,14 @@ RSpec.describe API::Search do
end
end
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
+ end
+ end
end
describe "GET /groups/:id/search" do
@@ -513,6 +521,14 @@ RSpec.describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
+ end
+ end
end
end
@@ -786,6 +802,14 @@ RSpec.describe API::Search do
end
end
end
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :user_email_lookup do
+ let(:current_user) { user }
+
+ def request
+ get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
+ end
+ end
end
end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index b17bc11a451..c0f04ba09be 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -154,6 +154,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
end
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file' do
+ let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
let(:tokens) do
{
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
@@ -202,7 +203,6 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
- let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
@@ -212,6 +212,41 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
+
+ context 'with package file pending destruction' do
+ let_it_be(:package) { create(:package, package_type: :terraform_module, project: project, name: "module-555/pending-destruction", version: '1.0.0') }
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package) }
+ let_it_be(:package_file) { create(:package_file, :terraform_module, package: package) }
+
+ let(:token) { tokens[:personal_access_token] }
+ let(:headers) { { 'Authorization' => "Bearer #{token}" } }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'does not return them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
+ expect(response.body).to eq(package_file.file.file.read)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(package_file_pending_destruction.file.file.read)
+ expect(response.body).not_to eq(package_file.file.file.read)
+ end
+ end
+ end
end
describe 'PUT /api/v4/projects/:project_id/packages/terraform/modules/:module_name/:module_system/:module_version/file/authorize' do
diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
index 225af57a267..0b73d0f96a4 100644
--- a/spec/requests/api/usage_data_non_sql_metrics_spec.rb
+++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe API::UsageDataNonSqlMetrics do
context 'with authentication' do
before do
stub_feature_flags(usage_data_non_sql_metrics: true)
+ stub_database_flavor_check
end
it 'returns non sql metrics if user is admin' do
diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb
index 0ba4a37bc9b..69a8d865a59 100644
--- a/spec/requests/api/usage_data_queries_spec.rb
+++ b/spec/requests/api/usage_data_queries_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe API::UsageDataQueries do
before do
stub_usage_data_connections
+ stub_database_flavor_check
end
describe 'GET /usage_data/usage_data_queries' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index b93df2f3bae..98875d7e8d2 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -498,6 +498,10 @@ RSpec.describe API::Users do
describe "GET /users/:id" do
let_it_be(:user2, reload: true) { create(:user, username: 'another_user') }
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:users_get_by_id, scope: user).and_return(false)
+ end
+
it "returns a user by id" do
get api("/users/#{user.id}", user)
@@ -593,6 +597,55 @@ RSpec.describe API::Users do
expect(json_response).not_to have_key('sign_in_count')
end
+ context 'when the rate limit is not exceeded' do
+ it 'returns a success status' do
+ expect(Gitlab::ApplicationRateLimiter)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user)
+ .and_return(false)
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the rate limit is exceeded' do
+ context 'when feature flag is enabled' do
+ it 'returns "too many requests" status' do
+ expect(Gitlab::ApplicationRateLimiter)
+ .to receive(:throttled?).with(:users_get_by_id, scope: user)
+ .and_return(true)
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+
+ it 'still allows admin users' do
+ expect(Gitlab::ApplicationRateLimiter)
+ .not_to receive(:throttled?)
+
+ get api("/users/#{user.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(rate_limit_user_by_id_endpoint: false)
+ end
+
+ it 'does not throttle the request' do
+ expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?)
+
+ get api("/users/#{user.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
context 'when job title is present' do
let(:job_title) { 'Fullstack Engineer' }
@@ -974,7 +1027,7 @@ RSpec.describe API::Users do
post api('/users', admin),
params: {
email: 'invalid email',
- password: 'password',
+ password: Gitlab::Password.test_default,
name: 'test'
}
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1040,7 +1093,7 @@ RSpec.describe API::Users do
post api('/users', admin),
params: {
email: 'test@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'test',
name: 'foo'
}
@@ -1052,7 +1105,7 @@ RSpec.describe API::Users do
params: {
name: 'foo',
email: 'test@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'foo'
}
end.to change { User.count }.by(0)
@@ -1066,7 +1119,7 @@ RSpec.describe API::Users do
params: {
name: 'foo',
email: 'foo@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'test'
}
end.to change { User.count }.by(0)
@@ -1080,7 +1133,7 @@ RSpec.describe API::Users do
params: {
name: 'foo',
email: 'foo@example.com',
- password: 'password',
+ password: Gitlab::Password.test_default,
username: 'TEST'
}
end.to change { User.count }.by(0)
@@ -1425,8 +1478,8 @@ RSpec.describe API::Users do
context "with existing user" do
before do
- post api("/users", admin), params: { email: 'test@example.com', password: 'password', username: 'test', name: 'test' }
- post api("/users", admin), params: { email: 'foo@bar.com', password: 'password', username: 'john', name: 'john' }
+ post api("/users", admin), params: { email: 'test@example.com', password: Gitlab::Password.test_default, username: 'test', name: 'test' }
+ post api("/users", admin), params: { email: 'foo@bar.com', password: Gitlab::Password.test_default, username: 'john', name: 'john' }
@user = User.all.last
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index d2528600477..623cf24b9cb 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -319,7 +319,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when user is using credentials with special characters' do
context 'with password with special characters' do
before do
- user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
+ user.update!(password: Gitlab::Password.test_default)
end
it 'allows clones' do
@@ -1670,7 +1670,7 @@ RSpec.describe 'Git HTTP requests' do
context 'when user is using credentials with special characters' do
context 'with password with special characters' do
before do
- user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
+ user.update!(password: Gitlab::Password.test_default)
end
it 'allows clones' do
diff --git a/spec/requests/groups/crm/contacts_controller_spec.rb b/spec/requests/groups/crm/contacts_controller_spec.rb
index a4b2a28e77a..5d126c6ead5 100644
--- a/spec/requests/groups/crm/contacts_controller_spec.rb
+++ b/spec/requests/groups/crm/contacts_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Groups::Crm::ContactsController do
shared_examples 'ok response with index template if authorized' do
context 'private group' do
- let(:group) { create(:group, :private) }
+ let(:group) { create(:group, :private, :crm_enabled) }
context 'with authorized user' do
before do
@@ -32,11 +32,17 @@ RSpec.describe Groups::Crm::ContactsController do
sign_in(user)
end
- context 'when feature flag is enabled' do
+ context 'when crm_enabled is true' do
it_behaves_like 'ok response with index template'
end
- context 'when feature flag is not enabled' do
+ context 'when crm_enabled is false' do
+ let(:group) { create(:group, :private) }
+
+ it_behaves_like 'response with 404 status'
+ end
+
+ context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@@ -64,10 +70,10 @@ RSpec.describe Groups::Crm::ContactsController do
end
context 'public group' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do
- it_behaves_like 'ok response with index template'
+ it_behaves_like 'response with 404 status'
end
end
end
diff --git a/spec/requests/groups/crm/organizations_controller_spec.rb b/spec/requests/groups/crm/organizations_controller_spec.rb
index 7595950350d..f38300c3c5b 100644
--- a/spec/requests/groups/crm/organizations_controller_spec.rb
+++ b/spec/requests/groups/crm/organizations_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Groups::Crm::OrganizationsController do
shared_examples 'ok response with index template if authorized' do
context 'private group' do
- let(:group) { create(:group, :private) }
+ let(:group) { create(:group, :private, :crm_enabled) }
context 'with authorized user' do
before do
@@ -32,11 +32,17 @@ RSpec.describe Groups::Crm::OrganizationsController do
sign_in(user)
end
- context 'when feature flag is enabled' do
+ context 'when crm_enabled is true' do
it_behaves_like 'ok response with index template'
end
- context 'when feature flag is not enabled' do
+ context 'when crm_enabled is false' do
+ let(:group) { create(:group, :private) }
+
+ it_behaves_like 'response with 404 status'
+ end
+
+ context 'when feature flag is disabled' do
before do
stub_feature_flags(customer_relations: false)
end
@@ -64,10 +70,10 @@ RSpec.describe Groups::Crm::OrganizationsController do
end
context 'public group' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :crm_enabled) }
context 'with anonymous user' do
- it_behaves_like 'ok response with index template'
+ it_behaves_like 'response with 404 status'
end
end
end
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
new file mode 100644
index 00000000000..eabdef3c41e
--- /dev/null
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Settings::AccessTokensController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:resource) { create(:group) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+
+ before_all do
+ resource.add_owner(user)
+ resource.add_maintainer(bot_user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'feature unavailable' do
+ context 'user is not a owner' do
+ before do
+ resource.add_maintainer(user)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe 'GET /:namespace/-/settings/access_tokens' do
+ subject do
+ get group_settings_access_tokens_path(resource)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'GET resource access tokens available'
+ end
+
+ describe 'POST /:namespace/-/settings/access_tokens' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
+
+ subject do
+ post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'POST resource access tokens available'
+
+ context 'when group access token creation is disabled' do
+ before do
+ resource.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+
+ it 'does not create the token' do
+ expect { subject }.not_to change { PersonalAccessToken.count }
+ end
+
+ it 'does not add the project bot as a member' do
+ expect { subject }.not_to change { Member.count }
+ end
+
+ it 'does not create the project bot user' do
+ expect { subject }.not_to change { User.count }
+ end
+ end
+
+ context 'with custom access level' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
+
+ subject { post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
+
+ it_behaves_like 'POST resource access tokens available'
+ end
+ end
+
+ describe 'PUT /:namespace/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
+
+ subject do
+ put revoke_group_settings_access_token_path(resource, resource_access_token)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'PUT resource access tokens available'
+ end
+end
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
new file mode 100644
index 00000000000..a5eccc43147
--- /dev/null
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::GoogleCloud::DeploymentsController do
+ let_it_be(:project) { create(:project, :public) }
+
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_developer) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
+ let_it_be(:user_creator) { project.creator }
+
+ let_it_be(:unauthorized_members) { [user_guest, user_developer] }
+ let_it_be(:authorized_members) { [user_maintainer, user_creator] }
+
+ let_it_be(:urls_list) { %W[#{project_google_cloud_deployments_cloud_run_path(project)} #{project_google_cloud_deployments_cloud_storage_path(project)}] }
+
+ before do
+ project.add_guest(user_guest)
+ project.add_developer(user_developer)
+ project.add_maintainer(user_maintainer)
+ end
+
+ describe "Routes must be restricted behind Google OAuth2" do
+ context 'when a public request is made' do
+ it 'returns not found on GET request' do
+ urls_list.each do |url|
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when unauthorized members make requests' do
+ it 'returns not found on GET request' do
+ urls_list.each do |url|
+ unauthorized_members.each do |unauthorized_member|
+ sign_in(unauthorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ context 'when authorized members make requests' do
+ it 'redirects on GET request' do
+ urls_list.each do |url|
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Authorized GET project/-/google_cloud/deployments/cloud_run' do
+ let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
+
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ end
+ end
+
+ it 'renders placeholder' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage' do
+ let_it_be(:url) { "#{project_google_cloud_deployments_cloud_storage_path(project)}" }
+
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ end
+ end
+
+ it 'renders placeholder' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
index 434e6f19ff5..7be863aae75 100644
--- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
def collection_arguments(pagination_data = {})
{
- environment: nil,
merge_request: merge_request,
commit: nil,
diff_view: :inline,
diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb
index ad50c39c65d..e17be1ff984 100644
--- a/spec/requests/projects/merge_requests/diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_spec.rb
@@ -29,7 +29,6 @@ RSpec.describe 'Merge Requests Diffs' do
def collection_arguments(pagination_data = {})
{
- environment: nil,
merge_request: merge_request,
commit: nil,
diff_view: :inline,
@@ -110,21 +109,6 @@ RSpec.describe 'Merge Requests Diffs' do
end
end
- context 'with a new environment' do
- let(:environment) do
- create(:environment, :available, project: project)
- end
-
- let!(:deployment) do
- create(:deployment, :success, environment: environment, ref: merge_request.source_branch)
- end
-
- it_behaves_like 'serializes diffs with expected arguments' do
- let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(environment: environment) }
- end
- end
-
context 'with disabled display_merge_conflicts_in_diff feature' do
before do
stub_feature_flags(display_merge_conflicts_in_diff: false)
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index 4921a43ab8b..6cf7bfb1795 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -244,7 +244,7 @@ RSpec.describe 'merge requests discussions' do
context 'when current_user role changes' do
before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.project_member(user))
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
end
it_behaves_like 'cache miss' do
diff --git a/spec/controllers/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
index 834a9e276f9..780d1b8caef 100644
--- a/spec/controllers/projects/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-require('spec_helper')
+require 'spec_helper'
RSpec.describe Projects::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:resource) { create(:project, group: group) }
let_it_be(:bot_user) { create(:user, :project_bot) }
before_all do
- project.add_maintainer(user)
- project.add_maintainer(bot_user)
+ resource.add_maintainer(user)
+ resource.add_maintainer(bot_user)
end
before do
@@ -20,34 +20,40 @@ RSpec.describe Projects::Settings::AccessTokensController do
shared_examples 'feature unavailable' do
context 'user is not a maintainer' do
before do
- project.add_developer(user)
+ resource.add_developer(user)
end
- it { is_expected.to have_gitlab_http_status(:not_found) }
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
end
end
- describe '#index' do
- subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+ describe 'GET /:namespace/:project/-/settings/access_tokens' do
+ subject do
+ get project_settings_access_tokens_path(resource)
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #index'
+ it_behaves_like 'GET resource access tokens available'
end
- describe '#create' do
+ describe 'POST /:namespace/:project/-/settings/access_tokens' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
- subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+ subject do
+ post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #create'
+ it_behaves_like 'POST resource access tokens available'
context 'when project access token creation is disabled' do
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
- it { is_expected.to have_gitlab_http_status(:not_found) }
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
it 'does not create the token' do
expect { subject }.not_to change { PersonalAccessToken.count }
@@ -65,18 +71,21 @@ RSpec.describe Projects::Settings::AccessTokensController do
context 'with custom access level' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
- subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+ subject { post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
- it_behaves_like 'project access tokens available #create'
+ it_behaves_like 'POST resource access tokens available'
end
end
- describe '#revoke', :sidekiq_inline do
- let(:project_access_token) { create(:personal_access_token, user: bot_user) }
+ describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
- subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
+ subject do
+ put revoke_project_settings_access_token_path(resource, resource_access_token)
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #revoke'
+ it_behaves_like 'PUT resource access tokens available'
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 244ec111a0c..793438808a5 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -499,9 +499,7 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
before do
group.add_owner(user)
- group.create_dependency_proxy_setting!(enabled: true)
other_group.add_owner(other_user)
- other_group.create_dependency_proxy_setting!(enabled: true)
allow(Gitlab.config.dependency_proxy)
.to receive(:enabled).and_return(true)
@@ -533,16 +531,10 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
context 'getting a blob' do
let_it_be(:blob) { create(:dependency_proxy_blob) }
+ let_it_be(:other_blob) { create(:dependency_proxy_blob) }
- let(:path) { "/v2/#{group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
- let(:other_path) { "/v2/#{other_group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
- let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
-
- before do
- allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
- allow(instance).to receive(:execute).and_return(blob_response)
- end
- end
+ let(:path) { "/v2/#{blob.group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
+ let(:other_path) { "/v2/#{other_blob.group.path}/dependency_proxy/containers/alpine/blobs/sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e" }
it_behaves_like 'rate-limited token-authenticated requests'
end
diff --git a/spec/requests/recursive_webhook_detection_spec.rb b/spec/requests/recursive_webhook_detection_spec.rb
new file mode 100644
index 00000000000..a3014bf1d73
--- /dev/null
+++ b/spec/requests/recursive_webhook_detection_spec.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_redis_shared_state, :request_store do
+ include StubRequests
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, namespace: user.namespace, creator: user) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:project_hook) { create(:project_hook, project: project, merge_requests_events: true) }
+ let_it_be(:system_hook) { create(:system_hook, merge_requests_events: true) }
+
+ # Trigger a change to the merge request to fire the webhooks.
+ def trigger_web_hooks
+ params = { merge_request: { description: FFaker::Lorem.sentence } }
+ put project_merge_request_path(project, merge_request), params: params, headers: headers
+ end
+
+ def stub_requests
+ stub_full_request(project_hook.url, method: :post, ip_address: '8.8.8.8')
+ stub_full_request(system_hook.url, method: :post, ip_address: '8.8.8.9')
+ end
+
+ before do
+ login_as(user)
+ end
+
+ context 'when the request headers include the recursive webhook detection header' do
+ let(:uuid) { SecureRandom.uuid }
+ let(:headers) { { Gitlab::WebHooks::RecursionDetection::UUID::HEADER => uuid } }
+
+ it 'executes all webhooks, logs no errors, and the webhook requests contain the same UUID header', :aggregate_failures do
+ stub_requests
+
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+
+ trigger_web_hooks
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
+ .once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
+ .with { |req| req.headers['X-Gitlab-Event-Uuid'] == uuid }
+ .once
+ end
+
+ context 'when one of the webhooks is recursive' do
+ before do
+ # Recreate the necessary state for the previous request to be
+ # considered made from the webhook.
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
+ Gitlab::WebHooks::RecursionDetection.register!(project_hook)
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
+ end
+
+ it 'executes all webhooks and logs an error for the recursive hook', :aggregate_failures do
+ stub_requests
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ recursion_detection: {
+ uuid: uuid,
+ ids: [project_hook.id]
+ }
+ )
+ ).twice # Twice: once in `#async_execute`, and again in `#execute`.
+
+ trigger_web_hooks
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
+ end
+ end
+
+ context 'when the count limit has been reached' do
+ let_it_be(:previous_hooks) { create_list(:project_hook, 3) }
+
+ before do
+ stub_const('Gitlab::WebHooks::RecursionDetection::COUNT_LIMIT', 2)
+ # Recreate the necessary state for a number of previous webhooks to
+ # have been triggered previously.
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
+ previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(nil)
+ end
+
+ it 'executes and logs errors for all hooks', :aggregate_failures do
+ stub_requests
+ previous_hook_ids = previous_hooks.map(&:id)
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ recursion_detection: {
+ uuid: uuid,
+ ids: include(*previous_hook_ids)
+ }
+ )
+ ).twice
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: system_hook.id,
+ recursion_detection: {
+ uuid: uuid,
+ ids: include(*previous_hook_ids)
+ }
+ )
+ ).twice
+
+ trigger_web_hooks
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).once
+ end
+ end
+ end
+
+ context 'when the recursive webhook detection header is absent' do
+ let(:headers) { {} }
+
+ let(:uuid_header_spy) do
+ Class.new do
+ attr_reader :values
+
+ def initialize
+ @values = []
+ end
+
+ def to_proc
+ proc do |method, *args|
+ method.call(*args).tap do |headers|
+ @values << headers[Gitlab::WebHooks::RecursionDetection::UUID::HEADER]
+ end
+ end
+ end
+ end.new
+ end
+
+ before do
+ allow(Gitlab::WebHooks::RecursionDetection).to receive(:header).at_least(:once).and_wrap_original(&uuid_header_spy)
+ end
+
+ it 'executes all webhooks, logs no errors, and the webhook requests contain different UUID headers', :aggregate_failures do
+ stub_requests
+
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+
+ trigger_web_hooks
+
+ uuid_headers = uuid_header_spy.values
+
+ expect(uuid_headers).to all(be_present)
+ expect(uuid_headers.uniq.length).to eq(2)
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
+ .once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url))
+ .with { |req| uuid_headers.include?(req.headers['X-Gitlab-Event-Uuid']) }
+ .once
+ end
+
+ it 'uses new UUID values between requests' do
+ stub_requests
+
+ trigger_web_hooks
+ trigger_web_hooks
+
+ uuid_headers = uuid_header_spy.values
+
+ expect(uuid_headers).to all(be_present)
+ expect(uuid_headers.length).to eq(4)
+ expect(uuid_headers.uniq.length).to eq(4)
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).twice
+ expect(WebMock).to have_requested(:post, stubbed_hostname(system_hook.url)).twice
+ end
+ end
+end
diff --git a/spec/requests/sandbox_controller_spec.rb b/spec/requests/sandbox_controller_spec.rb
new file mode 100644
index 00000000000..4fc26580123
--- /dev/null
+++ b/spec/requests/sandbox_controller_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SandboxController do
+ describe 'GET #mermaid' do
+ it 'renders page without template' do
+ get sandbox_mermaid_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(layout: nil)
+ end
+ end
+end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index eefc24f7824..dacc11eece7 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -636,6 +636,8 @@ RSpec.describe UsersController do
describe 'GET #exists' do
before do
sign_in(user)
+
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
context 'when user exists' do
@@ -677,6 +679,17 @@ RSpec.describe UsersController do
end
end
end
+
+ context 'when the rate limit has been reached' do
+ it 'returns status 429 Too Many Requests', :aggregate_failures do
+ ip = '1.2.3.4'
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:username_exists, scope: ip).and_return(true)
+
+ get user_exists_url(user.username), env: { 'REMOTE_ADDR': ip }
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+ end
end
describe '#ensure_canonical_path' do
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index e7ea5b79897..79edfdd2b3f 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -364,6 +364,12 @@ RSpec.describe AutocompleteController, 'routing' do
end
end
+RSpec.describe SandboxController, 'routing' do
+ it 'to #mermaid' do
+ expect(get("/-/sandbox/mermaid")).to route_to('sandbox#mermaid')
+ end
+end
+
RSpec.describe Snippets::BlobsController, "routing" do
it "to #raw" do
expect(get('/-/snippets/1/raw/master/lib/version.rb'))
diff --git a/spec/rubocop/code_reuse_helpers_spec.rb b/spec/rubocop/code_reuse_helpers_spec.rb
index 3220cff1681..d437ada85ee 100644
--- a/spec/rubocop/code_reuse_helpers_spec.rb
+++ b/spec/rubocop/code_reuse_helpers_spec.rb
@@ -315,76 +315,11 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end
end
- describe '#ee?' do
- before do
- stub_env('FOSS_ONLY', nil)
- allow(File).to receive(:exist?).with(ee_file_path) { true }
- end
-
- it 'returns true when ee/app/models/license.rb exists' do
- expect(cop.ee?).to eq(true)
- end
- end
-
- describe '#jh?' do
- context 'when jh directory exists and EE_ONLY is not set' do
- before do
- stub_env('EE_ONLY', nil)
-
- allow(Dir).to receive(:exist?).with(File.expand_path('../../jh', __dir__)) { true }
- end
-
- context 'when ee/app/models/license.rb exists' do
- before do
- allow(File).to receive(:exist?).with(ee_file_path) { true }
- end
-
- context 'when FOSS_ONLY is not set' do
- before do
- stub_env('FOSS_ONLY', nil)
- end
-
- it 'returns true' do
- expect(cop.jh?).to eq(true)
- end
- end
-
- context 'when FOSS_ONLY is set to 1' do
- before do
- stub_env('FOSS_ONLY', '1')
- end
+ %w[ee? jh?].each do |method_name|
+ it "delegates #{method_name} to GitlabEdition" do
+ expect(GitlabEdition).to receive(method_name)
- it 'returns false' do
- expect(cop.jh?).to eq(false)
- end
- end
- end
-
- context 'when ee/app/models/license.rb not exist' do
- before do
- allow(File).to receive(:exist?).with(ee_file_path) { false }
- end
-
- context 'when FOSS_ONLY is not set' do
- before do
- stub_env('FOSS_ONLY', nil)
- end
-
- it 'returns true' do
- expect(cop.jh?).to eq(false)
- end
- end
-
- context 'when FOSS_ONLY is set to 1' do
- before do
- stub_env('FOSS_ONLY', '1')
- end
-
- it 'returns false' do
- expect(cop.jh?).to eq(false)
- end
- end
- end
+ cop.public_send(method_name)
end
end
end
diff --git a/spec/rubocop/cop/database/establish_connection_spec.rb b/spec/rubocop/cop/database/establish_connection_spec.rb
new file mode 100644
index 00000000000..a3c27d33cb0
--- /dev/null
+++ b/spec/rubocop/cop/database/establish_connection_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../rubocop/cop/database/establish_connection'
+
+RSpec.describe RuboCop::Cop::Database::EstablishConnection do
+ subject(:cop) { described_class.new }
+
+ it 'flags the use of ActiveRecord::Base.establish_connection' do
+ expect_offense(<<~CODE)
+ ActiveRecord::Base.establish_connection
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't establish new database [...]
+ CODE
+ end
+
+ it 'flags the use of ActiveRecord::Base.establish_connection with arguments' do
+ expect_offense(<<~CODE)
+ ActiveRecord::Base.establish_connection(:foo)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't establish new database [...]
+ CODE
+ end
+
+ it 'flags the use of SomeModel.establish_connection' do
+ expect_offense(<<~CODE)
+ SomeModel.establish_connection
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't establish new database [...]
+ CODE
+ end
+end
diff --git a/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb b/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb
new file mode 100644
index 00000000000..aa63259288d
--- /dev/null
+++ b/spec/rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../../rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction'
+
+RSpec.describe RuboCop::Cop::Migration::PreventGlobalEnableLockRetriesWithDisableDdlTransaction do
+ subject(:cop) { described_class.new }
+
+ context 'when in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when `enable_lock_retries` and `disable_ddl_transaction` is used together' do
+ code = <<~RUBY
+ class SomeMigration < ActiveRecord::Migration[6.0]
+ enable_lock_retries!
+ disable_ddl_transaction!
+ end
+ RUBY
+
+ expect_offense(<<~RUBY, node: code, msg: described_class::MSG)
+ class SomeMigration < ActiveRecord::Migration[6.0]
+ enable_lock_retries!
+ disable_ddl_transaction!
+ ^^^^^^^^^^^^^^^^^^^^^^^^ %{msg}
+ end
+ RUBY
+ end
+
+ it 'registers no offense when `enable_lock_retries!` is used' do
+ expect_no_offenses(<<~RUBY)
+ class SomeMigration < ActiveRecord::Migration[6.0]
+ enable_lock_retries!
+ end
+ RUBY
+ end
+
+ it 'registers no offense when `disable_ddl_transaction!` is used' do
+ expect_no_offenses(<<~RUBY)
+ class SomeMigration < ActiveRecord::Migration[6.0]
+ disable_ddl_transaction!
+ end
+ RUBY
+ end
+ end
+
+ context 'when outside of migration' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class SomeMigration
+ enable_lock_retries!
+ disable_ddl_transaction!
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/schedule_async_spec.rb b/spec/rubocop/cop/migration/schedule_async_spec.rb
index b89acb6db41..5f848dd9b66 100644
--- a/spec/rubocop/cop/migration/schedule_async_spec.rb
+++ b/spec/rubocop/cop/migration/schedule_async_spec.rb
@@ -43,24 +43,18 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
end
context 'BackgroundMigrationWorker.perform_async' do
- it 'adds an offense when calling `BackgroundMigrationWorker.peform_async` and corrects', :aggregate_failures do
+ it 'adds an offense when calling `BackgroundMigrationWorker.peform_async`' do
expect_offense(<<~RUBY)
def up
BackgroundMigrationWorker.perform_async(ClazzName, "Bar", "Baz")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't call [...]
end
RUBY
-
- expect_correction(<<~RUBY)
- def up
- migrate_async(ClazzName, "Bar", "Baz")
- end
- RUBY
end
end
context 'BackgroundMigrationWorker.perform_in' do
- it 'adds an offense and corrects', :aggregate_failures do
+ it 'adds an offense' do
expect_offense(<<~RUBY)
def up
BackgroundMigrationWorker
@@ -68,17 +62,11 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
.perform_in(delay, ClazzName, "Bar", "Baz")
end
RUBY
-
- expect_correction(<<~RUBY)
- def up
- migrate_in(delay, ClazzName, "Bar", "Baz")
- end
- RUBY
end
end
context 'BackgroundMigrationWorker.bulk_perform_async' do
- it 'adds an offense and corrects', :aggregate_failures do
+ it 'adds an offense' do
expect_offense(<<~RUBY)
def up
BackgroundMigrationWorker
@@ -86,17 +74,11 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
.bulk_perform_async(jobs)
end
RUBY
-
- expect_correction(<<~RUBY)
- def up
- bulk_migrate_async(jobs)
- end
- RUBY
end
end
context 'BackgroundMigrationWorker.bulk_perform_in' do
- it 'adds an offense and corrects', :aggregate_failures do
+ it 'adds an offense' do
expect_offense(<<~RUBY)
def up
BackgroundMigrationWorker
@@ -104,12 +86,6 @@ RSpec.describe RuboCop::Cop::Migration::ScheduleAsync do
.bulk_perform_in(5.minutes, jobs)
end
RUBY
-
- expect_correction(<<~RUBY)
- def up
- bulk_migrate_in(5.minutes, jobs)
- end
- RUBY
end
end
end
diff --git a/spec/scripts/setup/find_jh_branch_spec.rb b/spec/scripts/setup/find_jh_branch_spec.rb
new file mode 100644
index 00000000000..dfc3601ffa9
--- /dev/null
+++ b/spec/scripts/setup/find_jh_branch_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+# NOTE: Under the context of fast_spec_helper, when we `require 'gitlab'`
+# we do not load the Gitlab client, but our own Gitlab module.
+# Keep this in mind and just stub anything which might touch it!
+require_relative '../../../scripts/setup/find-jh-branch'
+
+RSpec.describe FindJhBranch do
+ subject { described_class.new }
+
+ describe '#run' do
+ context 'when it is not a merge request' do
+ before do
+ expect(subject).to receive(:merge_request?).and_return(false)
+ end
+
+ it 'returns JH_DEFAULT_BRANCH' do
+ expect(subject.run).to eq(described_class::JH_DEFAULT_BRANCH)
+ end
+ end
+
+ context 'when it is a merge request' do
+ let(:branch_name) { 'branch-name' }
+ let(:jh_branch_name) { 'branch-name-jh' }
+ let(:default_branch) { 'main' }
+ let(:merge_request) { double(target_branch: target_branch) }
+ let(:target_branch) { default_branch }
+
+ before do
+ expect(subject).to receive(:merge_request?).and_return(true)
+
+ expect(subject)
+ .to receive(:branch_exist?)
+ .with(described_class::JH_PROJECT_PATH, jh_branch_name)
+ .and_return(jh_branch_exist)
+
+ allow(subject).to receive(:ref_name).and_return(branch_name)
+ allow(subject).to receive(:default_branch).and_return(default_branch)
+ allow(subject).to receive(:merge_request).and_return(merge_request)
+ end
+
+ context 'when there is a corresponding JH branch' do
+ let(:jh_branch_exist) { true }
+
+ it 'returns the corresponding JH branch name' do
+ expect(subject.run).to eq(jh_branch_name)
+ end
+ end
+
+ context 'when there is no corresponding JH branch' do
+ let(:jh_branch_exist) { false }
+
+ it 'returns the default JH branch' do
+ expect(subject.run).to eq(described_class::JH_DEFAULT_BRANCH)
+ end
+
+ context 'when it is targeting a default branch' do
+ let(:target_branch) { '14-6-stable-ee' }
+ let(:jh_stable_branch_name) { '14-6-stable-jh' }
+
+ before do
+ expect(subject)
+ .to receive(:branch_exist?)
+ .with(described_class::JH_PROJECT_PATH, jh_stable_branch_name)
+ .and_return(jh_stable_branch_exist)
+ end
+
+ context 'when there is a corresponding JH stable branch' do
+ let(:jh_stable_branch_exist) { true }
+
+ it 'returns the corresponding JH stable branch' do
+ expect(subject.run).to eq(jh_stable_branch_name)
+ end
+ end
+
+ context 'when there is no corresponding JH stable branch' do
+ let(:jh_stable_branch_exist) { false }
+
+ it "raises #{described_class::BranchNotFound}" do
+ expect { subject.run }.to raise_error(described_class::BranchNotFound)
+ end
+ end
+ end
+
+ context 'when it is not targeting the default branch' do
+ let(:target_branch) { default_branch.swapcase }
+
+ it 'returns the default JH branch' do
+ expect(subject.run).to eq(described_class::JH_DEFAULT_BRANCH)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
index 09804681f5d..b5678d91248 100644
--- a/spec/serializers/analytics_build_entity_spec.rb
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -27,6 +27,14 @@ RSpec.describe AnalyticsBuildEntity do
expect(subject).to include(:author)
end
+ it 'contains the project path' do
+ expect(subject).to include(:project_path)
+ end
+
+ it 'contains the namespace full path' do
+ expect(subject).to include(:namespace_full_path)
+ end
+
it 'does not contain sensitive information' do
expect(subject).not_to include(/token/)
expect(subject).not_to include(/variables/)
diff --git a/spec/serializers/analytics_issue_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb
index 447c5e7d02a..bc5cab638cd 100644
--- a/spec/serializers/analytics_issue_entity_spec.rb
+++ b/spec/serializers/analytics_issue_entity_spec.rb
@@ -32,6 +32,14 @@ RSpec.describe AnalyticsIssueEntity do
expect(subject).to include(:author)
end
+ it 'contains the project path' do
+ expect(subject).to include(:project_path)
+ end
+
+ it 'contains the namespace full path' do
+ expect(subject).to include(:namespace_full_path)
+ end
+
it 'does not contain sensitive information' do
expect(subject).not_to include(/token/)
expect(subject).not_to include(/variables/)
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 985e18f27a0..80b6f00d8c9 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -185,6 +185,42 @@ RSpec.describe EnvironmentSerializer do
end
end
+ context 'batching loading' do
+ let(:resource) { Environment.all }
+
+ before do
+ create(:environment, name: 'staging/review-1')
+ create_environment_with_associations(project)
+ end
+
+ it 'uses the custom preloader service' do
+ expect_next_instance_of(Preloaders::Environments::DeploymentPreloader) do |preloader|
+ expect(preloader).to receive(:execute_with_union).with(:last_deployment, hash_including(:deployable)).and_call_original
+ end
+
+ expect_next_instance_of(Preloaders::Environments::DeploymentPreloader) do |preloader|
+ expect(preloader).to receive(:execute_with_union).with(:upcoming_deployment, hash_including(:deployable)).and_call_original
+ end
+
+ json
+ end
+
+ # Including for test coverage pipeline failure, remove along with feature flag.
+ context 'when custom preload feature is disabled' do
+ before do
+ Feature.disable(:custom_preloader_for_deployments)
+ end
+
+ it 'avoids N+1 database queries' do
+ control_count = ActiveRecord::QueryRecorder.new { json }.count
+
+ create_environment_with_associations(project)
+
+ expect { json }.not_to exceed_query_limit(control_count)
+ end
+ end
+ end
+
def create_environment_with_associations(project)
create(:environment, project: project).tap do |environment|
create(:deployment, :success, environment: environment, project: project)
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index e4844c25067..59340181075 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -62,6 +62,10 @@ RSpec.describe GroupChildEntity do
expect(json[:edit_path]).to eq(edit_project_path(object))
end
+ it 'includes the last activity at' do
+ expect(json[:last_activity_at]).to be_present
+ end
+
it_behaves_like 'group child json'
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 587d167520f..f5398013a70 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -202,7 +202,7 @@ RSpec.describe PipelineSerializer do
# Existing numbers are high and require performance optimization
# Ongoing issue:
# https://gitlab.com/gitlab-org/gitlab/-/issues/225156
- expected_queries = Gitlab.ee? ? 74 : 70
+ expected_queries = Gitlab.ee? ? 78 : 74
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb
index 4b47efca9ed..35697ac79a0 100644
--- a/spec/services/alert_management/alerts/update_service_spec.rb
+++ b/spec/services/alert_management/alerts/update_service_spec.rb
@@ -235,6 +235,59 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
it_behaves_like 'adds a system note'
end
+
+ context 'with an associated issue' do
+ let_it_be(:issue, reload: true) { create(:issue, project: project) }
+
+ before do
+ alert.update!(issue: issue)
+ end
+
+ shared_examples 'does not sync with the incident status' do
+ specify do
+ expect(::Issues::UpdateService).not_to receive(:new)
+ expect { response }.to change { alert.acknowledged? }.to(true)
+ end
+ end
+
+ it_behaves_like 'does not sync with the incident status'
+
+ context 'when the issue is an incident' do
+ before do
+ issue.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it_behaves_like 'does not sync with the incident status'
+
+ context 'when the incident has an escalation status' do
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ it 'updates the incident escalation status with the new alert status' do
+ expect(::Issues::UpdateService).to receive(:new).once.and_call_original
+ expect(described_class).to receive(:new).once.and_call_original
+
+ expect { response }.to change { escalation_status.reload.acknowledged? }.to(true)
+ .and change { alert.reload.acknowledged? }.to(true)
+ end
+
+ context 'when the statuses match' do
+ before do
+ escalation_status.update!(status_event: :acknowledge)
+ end
+
+ it_behaves_like 'does not sync with the incident status'
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it_behaves_like 'does not sync with the incident status'
+ end
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb
index ce7b43972da..0379fd3f05c 100644
--- a/spec/services/audit_event_service_spec.rb
+++ b/spec/services/audit_event_service_spec.rb
@@ -60,17 +60,18 @@ RSpec.describe AuditEventService do
ip_address: user.current_sign_in_ip,
result: AuthenticationEvent.results[:success],
provider: 'standard'
- )
+ ).and_call_original
audit_service.for_authentication.security_event
end
it 'tracks exceptions when the event cannot be created' do
- allow(user).to receive_messages(current_sign_in_ip: 'invalid IP')
+ allow_next_instance_of(AuditEvent) do |event|
+ allow(event).to receive(:valid?).and_return(false)
+ end
expect(Gitlab::ErrorTracking).to(
- receive(:track_exception)
- .with(ActiveRecord::RecordInvalid, audit_event_type: 'AuthenticationEvent').and_call_original
+ receive(:track_and_raise_for_dev_exception)
)
audit_service.for_authentication.security_event
@@ -93,7 +94,7 @@ RSpec.describe AuditEventService do
end
specify do
- expect(AuthenticationEvent).to receive(:new).with(hash_including(ip_address: output))
+ expect(AuthenticationEvent).to receive(:new).with(hash_including(ip_address: output)).and_call_original
audit_service.for_authentication.security_event
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 46cc027fcb3..83f77780b80 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -92,6 +92,35 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'a modified token'
end
+
+ context 'with a project with a path with trailing underscore' do
+ let(:bad_project) { create(:project) }
+
+ before do
+ bad_project.update!(path: bad_project.path + '_')
+ bad_project.add_developer(current_user)
+ end
+
+ describe '#full_access_token' do
+ let(:token) { described_class.full_access_token(bad_project.full_path) }
+ let(:access) do
+ [{ 'type' => 'repository',
+ 'name' => bad_project.full_path,
+ 'actions' => ['*'],
+ 'migration_eligible' => false }]
+ end
+
+ subject { { token: token } }
+
+ it 'logs an exception and returns a valid access token' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ expect(token).to be_present
+ expect(payload).to be_a(Hash)
+ expect(payload).to include('access' => access)
+ end
+ end
+ end
end
context 'when not in migration mode' do
@@ -116,4 +145,28 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an unmodified token'
end
end
+
+ context 'CDN redirection' do
+ include_context 'container registry auth service context'
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_params) { { scopes: ["repository:#{project.full_path}:pull"] } }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a valid token'
+ it { expect(payload['access']).to include(include('cdn_redirect' => true)) }
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(container_registry_cdn_redirect: false)
+ end
+
+ it_behaves_like 'a valid token'
+ it { expect(payload['access']).not_to include(have_key('cdn_redirect')) }
+ end
+ end
end
diff --git a/spec/services/branches/delete_merged_service_spec.rb b/spec/services/branches/delete_merged_service_spec.rb
index 2cf0f53c8c3..46611670fe1 100644
--- a/spec/services/branches/delete_merged_service_spec.rb
+++ b/spec/services/branches/delete_merged_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Branches::DeleteMergedService do
include ProjectForksHelper
- subject(:service) { described_class.new(project, project.owner) }
+ subject(:service) { described_class.new(project, project.first_owner) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/bulk_imports/archive_extraction_service_spec.rb b/spec/services/bulk_imports/archive_extraction_service_spec.rb
index aa823d88010..da9df31cde9 100644
--- a/spec/services/bulk_imports/archive_extraction_service_spec.rb
+++ b/spec/services/bulk_imports/archive_extraction_service_spec.rb
@@ -34,9 +34,9 @@ RSpec.describe BulkImports::ArchiveExtractionService do
context 'when dir is not in tmpdir' do
it 'raises an error' do
- ['/etc', '/usr', '/', '/home', '', '/some/other/path', Rails.root].each do |path|
+ ['/etc', '/usr', '/', '/home', '/some/other/path', Rails.root.to_s].each do |path|
expect { described_class.new(tmpdir: path, filename: 'filename').execute }
- .to raise_error(BulkImports::Error, 'Invalid target directory')
+ .to raise_error(StandardError, "path #{path} is not allowed")
end
end
end
@@ -52,7 +52,7 @@ RSpec.describe BulkImports::ArchiveExtractionService do
context 'when filepath is being traversed' do
it 'raises an error' do
- expect { described_class.new(tmpdir: File.join(tmpdir, '../../../'), filename: 'name').execute }
+ expect { described_class.new(tmpdir: File.join(Dir.mktmpdir, 'test', '..'), filename: 'name').execute }
.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
end
end
diff --git a/spec/services/bulk_imports/file_decompression_service_spec.rb b/spec/services/bulk_imports/file_decompression_service_spec.rb
index 4e8f78c8243..1d6aa79a37f 100644
--- a/spec/services/bulk_imports/file_decompression_service_spec.rb
+++ b/spec/services/bulk_imports/file_decompression_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe BulkImports::FileDecompressionService do
FileUtils.remove_entry(tmpdir)
end
- subject { described_class.new(dir: tmpdir, filename: gz_filename) }
+ subject { described_class.new(tmpdir: tmpdir, filename: gz_filename) }
describe '#execute' do
it 'decompresses specified file' do
@@ -55,10 +55,18 @@ RSpec.describe BulkImports::FileDecompressionService do
end
context 'when dir is not in tmpdir' do
- subject { described_class.new(dir: '/etc', filename: 'filename') }
+ subject { described_class.new(tmpdir: '/etc', filename: 'filename') }
it 'raises an error' do
- expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid target directory')
+ expect { subject.execute }.to raise_error(StandardError, 'path /etc is not allowed')
+ end
+ end
+
+ context 'when path is being traversed' do
+ subject { described_class.new(tmpdir: File.join(Dir.mktmpdir, 'test', '..'), filename: 'filename') }
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
end
end
@@ -69,7 +77,7 @@ RSpec.describe BulkImports::FileDecompressionService do
FileUtils.ln_s(File.join(tmpdir, gz_filename), symlink)
end
- subject { described_class.new(dir: tmpdir, filename: 'symlink.gz') }
+ subject { described_class.new(tmpdir: tmpdir, filename: 'symlink.gz') }
it 'raises an error and removes the file' do
expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file')
@@ -87,7 +95,7 @@ RSpec.describe BulkImports::FileDecompressionService do
subject.instance_variable_set(:@decompressed_filepath, symlink)
end
- subject { described_class.new(dir: tmpdir, filename: gz_filename) }
+ subject { described_class.new(tmpdir: tmpdir, filename: gz_filename) }
it 'raises an error and removes the file' do
expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file')
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index a24af9ae64d..bd664d6e996 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe BulkImports::FileDownloadService do
described_class.new(
configuration: config,
relative_url: '/test',
- dir: tmpdir,
+ tmpdir: tmpdir,
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
@@ -72,7 +72,7 @@ RSpec.describe BulkImports::FileDownloadService do
service = described_class.new(
configuration: double,
relative_url: '/test',
- dir: tmpdir,
+ tmpdir: tmpdir,
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
@@ -157,7 +157,7 @@ RSpec.describe BulkImports::FileDownloadService do
described_class.new(
configuration: config,
relative_url: '/test',
- dir: tmpdir,
+ tmpdir: tmpdir,
filename: 'symlink',
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
@@ -179,7 +179,7 @@ RSpec.describe BulkImports::FileDownloadService do
described_class.new(
configuration: config,
relative_url: '/test',
- dir: '/etc',
+ tmpdir: '/etc',
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
@@ -188,8 +188,28 @@ RSpec.describe BulkImports::FileDownloadService do
it 'raises an error' do
expect { subject.execute }.to raise_error(
- described_class::ServiceError,
- 'Invalid target directory'
+ StandardError,
+ 'path /etc is not allowed'
+ )
+ end
+ end
+
+ context 'when dir path is being traversed' do
+ subject do
+ described_class.new(
+ configuration: config,
+ relative_url: '/test',
+ tmpdir: File.join(Dir.mktmpdir('bulk_imports'), 'test', '..'),
+ filename: filename,
+ file_size_limit: file_size_limit,
+ allowed_content_types: allowed_content_types
+ )
+ end
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(
+ Gitlab::Utils::PathTraversalAttackError,
+ 'Invalid path'
)
end
end
diff --git a/spec/services/bulk_imports/file_export_service_spec.rb b/spec/services/bulk_imports/file_export_service_spec.rb
index 0d129c75384..94efceff6c6 100644
--- a/spec/services/bulk_imports/file_export_service_spec.rb
+++ b/spec/services/bulk_imports/file_export_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe BulkImports::FileExportService do
let_it_be(:project) { create(:project) }
let_it_be(:export_path) { Dir.mktmpdir }
- let_it_be(:relation) { 'uploads' }
+ let_it_be(:relation) { BulkImports::FileTransfer::BaseConfig::UPLOADS_RELATION }
subject(:service) { described_class.new(project, export_path, relation) }
@@ -20,6 +20,20 @@ RSpec.describe BulkImports::FileExportService do
subject.execute
end
+ context 'when relation is lfs objects' do
+ let_it_be(:relation) { BulkImports::FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION }
+
+ it 'executes lfs objects export service' do
+ expect_next_instance_of(BulkImports::LfsObjectsExportService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ expect(subject).to receive(:tar_cf).with(archive: File.join(export_path, 'lfs_objects.tar'), dir: export_path)
+
+ subject.execute
+ end
+ end
+
context 'when unsupported relation is passed' do
it 'raises an error' do
service = described_class.new(project, export_path, 'unsupported')
diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
new file mode 100644
index 00000000000..5ae54ed309b
--- /dev/null
+++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::LfsObjectsExportService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:lfs_json_filename) { "#{BulkImports::FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION}.json" }
+ let_it_be(:remote_url) { 'http://my-object-storage.local' }
+
+ let(:export_path) { Dir.mktmpdir }
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+
+ subject(:service) { described_class.new(project, export_path) }
+
+ before do
+ stub_lfs_object_storage
+
+ %w(wiki design).each do |repository_type|
+ create(
+ :lfs_objects_project,
+ project: project,
+ repository_type: repository_type,
+ lfs_object: lfs_object
+ )
+ end
+
+ project.lfs_objects << lfs_object
+ end
+
+ after do
+ FileUtils.remove_entry(export_path) if Dir.exist?(export_path)
+ end
+
+ describe '#execute' do
+ it 'exports lfs objects and their repository types' do
+ filepath = File.join(export_path, lfs_json_filename)
+
+ service.execute
+
+ expect(File).to exist(File.join(export_path, lfs_object.oid))
+ expect(File).to exist(filepath)
+
+ lfs_json = Gitlab::Json.parse(File.read(filepath))
+
+ expect(lfs_json).to eq(
+ {
+ lfs_object.oid => [
+ LfsObjectsProject.repository_types['wiki'],
+ LfsObjectsProject.repository_types['design'],
+ nil
+ ]
+ }
+ )
+ end
+
+ context 'when lfs object is remotely stored' do
+ let(:lfs_object) { create(:lfs_object, :object_storage) }
+
+ it 'downloads lfs object from object storage' do
+ expect_next_instance_of(LfsObjectUploader) do |instance|
+ expect(instance).to receive(:url).and_return(remote_url)
+ end
+
+ expect(subject).to receive(:download).with(remote_url, File.join(export_path, lfs_object.oid))
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
index b0bb741564d..53d90c7f100 100644
--- a/spec/services/chat_names/authorize_user_service_spec.rb
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe ChatNames::AuthorizeUserService do
describe '#execute' do
- subject { described_class.new(service, params) }
-
+ let(:integration) { create(:integration) }
let(:result) { subject.execute }
- let(:service) { create(:service) }
+
+ subject { described_class.new(integration, params) }
context 'when all parameters are valid' do
let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } }
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
index 9bbad09cd0d..4b0a1204558 100644
--- a/spec/services/chat_names/find_user_service_spec.rb
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
describe '#execute' do
- let(:integration) { create(:service) }
+ let(:integration) { create(:integration) }
subject { described_class.new(integration, params).execute }
diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/after_requeue_job_service_spec.rb
index 2465bac7d10..d2acf3ad2f1 100644
--- a/spec/services/ci/after_requeue_job_service_spec.rb
+++ b/spec/services/ci/after_requeue_job_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::AfterRequeueJobService do
let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
index b08ba6fd5e5..bf2e5302d2e 100644
--- a/spec/services/ci/archive_trace_service_spec.rb
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -15,6 +15,25 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
expect(job.trace_metadata.trace_artifact).to eq(job.job_artifacts_trace)
end
+ context 'integration hooks' do
+ it do
+ stub_feature_flags(datadog_integration_logs_collection: [job.project])
+
+ expect(job.project).to receive(:execute_integrations) do |data, hook_type|
+ expect(data).to eq Gitlab::DataBuilder::ArchiveTrace.build(job)
+ expect(hook_type).to eq :archive_trace_hooks
+ end
+ expect { subject }.not_to raise_error
+ end
+
+ it 'with feature flag disabled' do
+ stub_feature_flags(datadog_integration_logs_collection: false)
+
+ expect(job.project).not_to receive(:execute_integrations)
+ expect { subject }.not_to raise_error
+ end
+ end
+
context 'when trace is already archived' do
let!(:job) { create(:ci_build, :success, :trace_artifact) }
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index 2237fd76d07..d61abf6a6ee 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -604,7 +604,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
context 'when configured with bridge job rules' do
before do
stub_ci_pipeline_yaml_file(config)
- downstream_project.add_maintainer(upstream_project.owner)
+ downstream_project.add_maintainer(upstream_project.first_owner)
end
let(:config) do
@@ -622,13 +622,13 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
let(:primary_pipeline) do
- Ci::CreatePipelineService.new(upstream_project, upstream_project.owner, { ref: 'master' })
+ Ci::CreatePipelineService.new(upstream_project, upstream_project.first_owner, { ref: 'master' })
.execute(:push, save_on_errors: false)
.payload
end
let(:bridge) { primary_pipeline.processables.find_by(name: 'bridge-job') }
- let(:service) { described_class.new(upstream_project, upstream_project.owner) }
+ let(:service) { described_class.new(upstream_project, upstream_project.first_owner) }
context 'that include the bridge job' do
it 'creates the downstream pipeline' do
diff --git a/spec/services/ci/create_pipeline_service/cache_spec.rb b/spec/services/ci/create_pipeline_service/cache_spec.rb
index f5f162e4578..ca85a8cce64 100644
--- a/spec/services/ci/create_pipeline_service/cache_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cache_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
context 'cache' do
let(:project) { create(:project, :custom_repo, files: files) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
diff --git a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
index c69c91593ae..e62a94b6df8 100644
--- a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
+++ b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
describe 'creation errors and warnings' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
index f150a4f8b51..a0cbf14d936 100644
--- a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
+++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
diff --git a/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb b/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
index 026111d59f1..716a929830e 100644
--- a/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
+++ b/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
describe '!reference tags' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
diff --git a/spec/services/ci/create_pipeline_service/dry_run_spec.rb b/spec/services/ci/create_pipeline_service/dry_run_spec.rb
index ae43c63b516..9a7e97fb12b 100644
--- a/spec/services/ci/create_pipeline_service/dry_run_spec.rb
+++ b/spec/services/ci/create_pipeline_service/dry_run_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
diff --git a/spec/services/ci/create_pipeline_service/include_spec.rb b/spec/services/ci/create_pipeline_service/include_spec.rb
index aa01977272a..3116801d50c 100644
--- a/spec/services/ci/create_pipeline_service/include_spec.rb
+++ b/spec/services/ci/create_pipeline_service/include_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
context 'include:' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:variables_attributes) { [{ key: 'MYVAR', secret_value: 'hello' }] }
diff --git a/spec/services/ci/create_pipeline_service/logger_spec.rb b/spec/services/ci/create_pipeline_service/logger_spec.rb
index dfe0859015d..53e5f0dd7f2 100644
--- a/spec/services/ci/create_pipeline_service/logger_spec.rb
+++ b/spec/services/ci/create_pipeline_service/logger_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
context 'pipeline logger' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
@@ -35,7 +35,10 @@ RSpec.describe Ci::CreatePipelineService do
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
'pipeline_creation_duration_s' => counters,
'pipeline_size_count' => counters,
- 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => counters
+ 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => counters,
+ 'pipeline_seed_build_inclusion_duration_s' => counters,
+ 'pipeline_builds_tags_count' => a_kind_of(Numeric),
+ 'pipeline_builds_distinct_tags_count' => a_kind_of(Numeric)
}
end
@@ -81,7 +84,6 @@ RSpec.describe Ci::CreatePipelineService do
{
'pipeline_creation_caller' => 'Ci::CreatePipelineService',
'pipeline_source' => 'push',
- 'pipeline_id' => nil,
'pipeline_persisted' => false,
'project_id' => project.id,
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
diff --git a/spec/services/ci/create_pipeline_service/merge_requests_spec.rb b/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
index a1f85faa69f..de19ef363fb 100644
--- a/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
+++ b/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
context 'merge requests handling' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/feature' }
let(:source) { :push }
diff --git a/spec/services/ci/create_pipeline_service/needs_spec.rb b/spec/services/ci/create_pipeline_service/needs_spec.rb
index 9070d86f7f6..abd17ccdd6a 100644
--- a/spec/services/ci/create_pipeline_service/needs_spec.rb
+++ b/spec/services/ci/create_pipeline_service/needs_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
context 'needs' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
diff --git a/spec/services/ci/create_pipeline_service/parallel_spec.rb b/spec/services/ci/create_pipeline_service/parallel_spec.rb
index 6b455bf4874..ae28b74fef5 100644
--- a/spec/services/ci/create_pipeline_service/parallel_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parallel_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:service) { described_class.new(project, user, { ref: 'master' }) }
let(:pipeline) { service.execute(:push).payload }
diff --git a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
index 761504ffb58..c28bc9d8c13 100644
--- a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) }
let(:content) do
diff --git a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
index 5e34eeb99db..c6e69862422 100644
--- a/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
+++ b/spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
describe '.pre/.post stages' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index d0915f099de..d0ce1c5aba8 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
diff --git a/spec/services/ci/create_pipeline_service/tags_spec.rb b/spec/services/ci/create_pipeline_service/tags_spec.rb
index cbbeb870c5f..61c2415fa33 100644
--- a/spec/services/ci/create_pipeline_service/tags_spec.rb
+++ b/spec/services/ci/create_pipeline_service/tags_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
describe 'tags:' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ef879d536c3..a7026f5062e 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Ci::CreatePipelineService do
include ProjectForksHelper
let_it_be_with_refind(:project) { create(:project, :repository) }
- let_it_be_with_reload(:user) { project.owner }
+ let_it_be_with_reload(:user) { project.first_owner }
let(:ref_name) { 'refs/heads/master' }
@@ -146,140 +146,22 @@ RSpec.describe Ci::CreatePipelineService do
end
context 'when merge requests already exist for this source branch' do
- let(:merge_request_1) do
+ let!(:merge_request_1) do
create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project)
end
- let(:merge_request_2) do
+ let!(:merge_request_2) do
create(:merge_request, source_branch: 'feature', target_branch: "v1.1.0", source_project: project)
end
- context 'when related merge request is already merged' do
- let!(:merged_merge_request) do
- create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged')
- end
-
- it 'does not schedule update head pipeline job' do
- expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id)
-
- execute_service
- end
- end
-
context 'when the head pipeline sha equals merge request sha' do
it 'updates head pipeline of each merge request', :sidekiq_might_not_need_inline do
- merge_request_1
- merge_request_2
-
head_pipeline = execute_service(ref: 'feature', after: nil).payload
expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
end
end
-
- context 'when the head pipeline sha does not equal merge request sha' do
- it 'does not update the head piepeline of MRs' do
- merge_request_1
- merge_request_2
-
- allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true)
-
- expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.not_to raise_error
-
- last_pipeline = Ci::Pipeline.last
-
- expect(merge_request_1.reload.head_pipeline).not_to eq(last_pipeline)
- expect(merge_request_2.reload.head_pipeline).not_to eq(last_pipeline)
- end
- end
-
- context 'when there is no pipeline for source branch' do
- it "does not update merge request head pipeline" do
- merge_request = create(:merge_request, source_branch: 'feature',
- target_branch: "branch_1",
- source_project: project)
-
- head_pipeline = execute_service.payload
-
- expect(merge_request.reload.head_pipeline).not_to eq(head_pipeline)
- end
- end
-
- context 'when merge request target project is different from source project' do
- let!(:project) { fork_project(target_project, nil, repository: true) }
- let!(:target_project) { create(:project, :repository) }
- let!(:user) { create(:user) }
-
- before do
- project.add_developer(user)
- end
-
- it 'updates head pipeline for merge request', :sidekiq_might_not_need_inline do
- merge_request = create(:merge_request, source_branch: 'feature',
- target_branch: "master",
- source_project: project,
- target_project: target_project)
-
- head_pipeline = execute_service(ref: 'feature', after: nil).payload
-
- expect(merge_request.reload.head_pipeline).to eq(head_pipeline)
- end
- end
-
- context 'when the pipeline is not the latest for the branch' do
- it 'does not update merge request head pipeline' do
- merge_request = create(:merge_request, source_branch: 'master',
- target_branch: "branch_1",
- source_project: project)
-
- allow_any_instance_of(MergeRequest)
- .to receive(:find_actual_head_pipeline) { }
-
- execute_service
-
- expect(merge_request.reload.head_pipeline).to be_nil
- end
- end
-
- context 'when pipeline has errors' do
- before do
- stub_ci_pipeline_yaml_file('some invalid syntax')
- end
-
- it 'updates merge request head pipeline reference', :sidekiq_might_not_need_inline do
- merge_request = create(:merge_request, source_branch: 'master',
- target_branch: 'feature',
- source_project: project)
-
- head_pipeline = execute_service.payload
-
- expect(head_pipeline).to be_persisted
- expect(head_pipeline.yaml_errors).to be_present
- expect(head_pipeline.messages).to be_present
- expect(merge_request.reload.head_pipeline).to eq head_pipeline
- end
- end
-
- context 'when pipeline has been skipped' do
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:git_commit_message)
- .and_return('some commit [ci skip]')
- end
-
- it 'updates merge request head pipeline', :sidekiq_might_not_need_inline do
- merge_request = create(:merge_request, source_branch: 'master',
- target_branch: 'feature',
- source_project: project)
-
- head_pipeline = execute_service.payload
-
- expect(head_pipeline).to be_skipped
- expect(head_pipeline).to be_persisted
- expect(merge_request.reload.head_pipeline).to eq head_pipeline
- end
- end
end
context 'auto-cancel enabled' do
@@ -1655,7 +1537,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(pipeline.target_sha).to be_nil
end
- it 'schedules update for the head pipeline of the merge request' do
+ it 'schedules update for the head pipeline of the merge request', :sidekiq_inline do
expect(UpdateHeadPipelineForMergeRequestWorker)
.to receive(:perform_async).with(merge_request.id)
diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
index 6c1c02b2875..045051c7152 100644
--- a/spec/services/ci/destroy_pipeline_service_spec.rb
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe ::Ci::DestroyPipelineService do
subject { described_class.new(project, user).execute(pipeline) }
context 'user is owner' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
it 'destroys the pipeline' do
subject
@@ -66,6 +66,28 @@ RSpec.describe ::Ci::DestroyPipelineService do
expect { subject }.to change { Ci::DeletedObject.count }
end
end
+
+ context 'when job has trace chunks' do
+ let(:connection_params) { Gitlab.config.artifacts.object_store.connection.symbolize_keys }
+ let(:connection) { ::Fog::Storage.new(connection_params) }
+
+ before do
+ stub_object_storage(connection_params: connection_params, remote_directory: 'artifacts')
+ stub_artifacts_object_storage
+ end
+
+ let!(:trace_chunk) { create(:ci_build_trace_chunk, :fog_with_data, build: build) }
+
+ it 'destroys associated trace chunks' do
+ subject
+
+ expect { trace_chunk.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'removes data from object store' do
+ expect { subject }.to change { Ci::BuildTraceChunks::Fog.new.data(trace_chunk) }
+ end
+ end
end
context 'when pipeline is in cancelable state' do
diff --git a/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb b/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb
new file mode 100644
index 00000000000..74fa42962f3
--- /dev/null
+++ b/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobArtifacts::DeleteProjectArtifactsService do
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.new(project: project) }
+
+ describe '#execute' do
+ it 'enqueues a Ci::ExpireProjectBuildArtifactsWorker' do
+ expect(Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker).to receive(:perform_async).with(project.id).and_call_original
+
+ subject.execute
+ end
+ end
+end
diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
index e71f1a4266a..e95a449d615 100644
--- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
context 'with preloaded relationships' do
before do
- stub_const("#{described_class}::LOOP_LIMIT", 1)
+ stub_const("#{described_class}::LARGE_LOOP_LIMIT", 1)
end
context 'with ci_destroy_unlocked_job_artifacts feature flag disabled' do
@@ -53,46 +53,6 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
log = ActiveRecord::QueryRecorder.new { subject }
expect(log.count).to be_within(1).of(8)
end
-
- context 'with several locked-unknown artifact records' do
- before do
- stub_const("#{described_class}::LOOP_LIMIT", 10)
- stub_const("#{described_class}::BATCH_SIZE", 2)
- end
-
- let!(:lockable_artifact_records) do
- [
- create(:ci_job_artifact, :metadata, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: locked_job),
- create(:ci_job_artifact, :junit, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: locked_job),
- create(:ci_job_artifact, :sast, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: locked_job),
- create(:ci_job_artifact, :cobertura, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: locked_job),
- create(:ci_job_artifact, :trace, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: locked_job)
- ]
- end
-
- let!(:unlockable_artifact_records) do
- [
- create(:ci_job_artifact, :metadata, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: job),
- create(:ci_job_artifact, :junit, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: job),
- create(:ci_job_artifact, :sast, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: job),
- create(:ci_job_artifact, :cobertura, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: job),
- create(:ci_job_artifact, :trace, :expired, locked: ::Ci::JobArtifact.lockeds[:unknown], job: job),
- artifact
- ]
- end
-
- it 'updates the locked status of job artifacts from artifacts-locked pipelines' do
- subject
-
- expect(lockable_artifact_records).to be_all(&:persisted?)
- expect(lockable_artifact_records).to be_all { |artifact| artifact.reload.artifact_artifacts_locked? }
- end
-
- it 'unlocks and then destroys job artifacts from artifacts-unlocked pipelines' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(-6)
- expect(Ci::JobArtifact.where(id: unlockable_artifact_records.map(&:id))).to be_empty
- end
- end
end
end
@@ -159,7 +119,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do
- stub_const("#{described_class}::LOOP_LIMIT", 10)
+ stub_const("#{described_class}::LARGE_LOOP_LIMIT", 10)
end
context 'when the import fails' do
@@ -229,7 +189,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
context 'when loop reached loop limit' do
before do
- stub_const("#{described_class}::LOOP_LIMIT", 1)
+ stub_feature_flags(ci_artifact_fast_removal_large_loop_limit: false)
+ stub_const("#{described_class}::SMALL_LOOP_LIMIT", 1)
end
it 'destroys one artifact' do
diff --git a/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb b/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb
new file mode 100644
index 00000000000..fb9dd6b876b
--- /dev/null
+++ b/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline, reload: true) { create(:ci_pipeline, :unlocked, project: project) }
+
+ let(:expiry_time) { Time.current }
+
+ RSpec::Matchers.define :have_locked_status do |expected_status|
+ match do |job_artifacts|
+ predicate = "#{expected_status}?".to_sym
+ job_artifacts.all? { |artifact| artifact.__send__(predicate) }
+ end
+ end
+
+ RSpec::Matchers.define :expire_at do |expected_expiry|
+ match do |job_artifacts|
+ job_artifacts.all? { |artifact| artifact.expire_at.to_i == expected_expiry.to_i }
+ end
+ end
+
+ RSpec::Matchers.define :have_no_expiry do
+ match do |job_artifacts|
+ job_artifacts.all? { |artifact| artifact.expire_at.nil? }
+ end
+ end
+
+ describe '#execute' do
+ subject(:execute) { described_class.new(project.id, expiry_time).execute }
+
+ context 'with job containing erasable artifacts' do
+ let_it_be(:job, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }
+
+ it 'unlocks erasable job artifacts' do
+ execute
+
+ expect(job.job_artifacts).to have_locked_status(:artifact_unlocked)
+ end
+
+ it 'expires erasable job artifacts' do
+ execute
+
+ expect(job.job_artifacts).to expire_at(expiry_time)
+ end
+ end
+
+ context 'with job containing trace artifacts' do
+ let_it_be(:job, reload: true) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ it 'does not unlock trace artifacts' do
+ execute
+
+ expect(job.job_artifacts).to have_locked_status(:artifact_unknown)
+ end
+
+ it 'does not expire trace artifacts' do
+ execute
+
+ expect(job.job_artifacts).to have_no_expiry
+ end
+ end
+
+ context 'with job from artifact locked pipeline' do
+ let_it_be(:job, reload: true) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:locked_artifact, reload: true) { create(:ci_job_artifact, :locked, job: job) }
+
+ before do
+ pipeline.artifacts_locked!
+ end
+
+ it 'does not unlock locked artifacts' do
+ execute
+
+ expect(job.job_artifacts).to have_locked_status(:artifact_artifacts_locked)
+ end
+
+ it 'does not expire locked artifacts' do
+ execute
+
+ expect(job.job_artifacts).to have_no_expiry
+ end
+ end
+
+ context 'with job containing both erasable and trace artifacts' do
+ let_it_be(:job, reload: true) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:erasable_artifact, reload: true) { create(:ci_job_artifact, :archive, job: job) }
+ let_it_be(:trace_artifact, reload: true) { create(:ci_job_artifact, :trace, job: job) }
+
+ it 'unlocks erasable artifacts' do
+ execute
+
+ expect(erasable_artifact.artifact_unlocked?).to be_truthy
+ end
+
+ it 'expires erasable artifacts' do
+ execute
+
+ expect(erasable_artifact.expire_at.to_i).to eq(expiry_time.to_i)
+ end
+
+ it 'does not unlock trace artifacts' do
+ execute
+
+ expect(trace_artifact.artifact_unlocked?).to be_falsey
+ end
+
+ it 'does not expire trace artifacts' do
+ execute
+
+ expect(trace_artifact.expire_at).to be_nil
+ end
+ end
+
+ context 'with multiple pipelines' do
+ let_it_be(:job, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }
+
+ let_it_be(:pipeline2, reload: true) { create(:ci_pipeline, :unlocked, project: project) }
+ let_it_be(:job2, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }
+
+ it 'unlocks artifacts across pipelines' do
+ execute
+
+ expect(job.job_artifacts).to have_locked_status(:artifact_unlocked)
+ expect(job2.job_artifacts).to have_locked_status(:artifact_unlocked)
+ end
+
+ it 'expires artifacts across pipelines' do
+ execute
+
+ expect(job.job_artifacts).to expire_at(expiry_time)
+ expect(job2.job_artifacts).to expire_at(expiry_time)
+ end
+ end
+
+ context 'with artifacts belonging to another project' do
+ let_it_be(:job, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }
+
+ let_it_be(:another_project, reload: true) { create(:project) }
+ let_it_be(:another_pipeline, reload: true) { create(:ci_pipeline, project: another_project) }
+ let_it_be(:another_job, reload: true) { create(:ci_build, :erasable, pipeline: another_pipeline) }
+
+ it 'does not unlock erasable artifacts in other projects' do
+ execute
+
+ expect(another_job.job_artifacts).to have_locked_status(:artifact_unknown)
+ end
+
+ it 'does not expire erasable artifacts in other projects' do
+ execute
+
+ expect(another_job.job_artifacts).to have_no_expiry
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 31e1b0a896d..26bc6f747e1 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
describe 'Pipeline Processing Service Tests With Yaml' do
let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
where(:test_file_path) do
Dir.glob(Rails.root.join('spec/services/ci/pipeline_processing/test_cases/*.yml'))
@@ -65,7 +65,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
describe 'Pipeline Processing Service' do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:pipeline) do
create(:ci_empty_pipeline, ref: 'master', project: project)
diff --git a/spec/services/ci/pipelines/add_job_service_spec.rb b/spec/services/ci/pipelines/add_job_service_spec.rb
index 709a840c644..560724a1c6a 100644
--- a/spec/services/ci/pipelines/add_job_service_spec.rb
+++ b/spec/services/ci/pipelines/add_job_service_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Ci::Pipelines::AddJobService do
execute
end.to change { job.slice(:pipeline, :project, :ref) }.to(
pipeline: pipeline, project: pipeline.project, ref: pipeline.ref
- )
+ ).and change { job.metadata.project }.to(pipeline.project)
end
it 'returns a service response with the job as payload' do
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 34f77260334..85ef8b60af4 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -122,7 +122,7 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
end
context 'when build is not a playable manual action' do
- let(:build) { create(:ci_build, when: :manual, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:branch) { create(:protected_branch, :developers_can_merge, name: build.ref, project: project) }
it 'duplicates the build' do
@@ -138,6 +138,18 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
expect(build.user).not_to eq user
expect(duplicate.user).to eq user
end
+
+ context 'and is not retryable' do
+ let(:build) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }
+
+ it 'does not duplicate the build' do
+ expect { service.execute(build) }.not_to change { Ci::Build.count }
+ end
+
+ it 'does not enqueue the build' do
+ expect { service.execute(build) }.not_to change { build.status }
+ end
+ end
end
context 'when build is not action' do
diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb
index 00b670ff54f..8b7717fe4bf 100644
--- a/spec/services/ci/process_sync_events_service_spec.rb
+++ b/spec/services/ci/process_sync_events_service_spec.rb
@@ -28,10 +28,10 @@ RSpec.describe Ci::ProcessSyncEventsService do
it 'consumes events' do
expect { execute }.to change(Projects::SyncEvent, :count).from(2).to(0)
- expect(project1.ci_project_mirror).to have_attributes(
+ expect(project1.reload.ci_project_mirror).to have_attributes(
namespace_id: parent_group_1.id
)
- expect(project2.ci_project_mirror).to have_attributes(
+ expect(project2.reload.ci_project_mirror).to have_attributes(
namespace_id: parent_group_2.id
)
end
@@ -71,6 +71,24 @@ RSpec.describe Ci::ProcessSyncEventsService do
expect { execute }.not_to change(Projects::SyncEvent, :count)
end
end
+
+ it 'does not delete non-executed events' do
+ new_project = create(:project)
+ sync_event_class.delete_all
+
+ project1.update!(group: parent_group_2)
+ new_project.update!(group: parent_group_1)
+ project2.update!(group: parent_group_1)
+
+ new_project_sync_event = new_project.sync_events.last
+
+ allow(sync_event_class).to receive(:preload_synced_relation).and_return(
+ sync_event_class.where.not(id: new_project_sync_event)
+ )
+
+ expect { execute }.to change(Projects::SyncEvent, :count).from(3).to(1)
+ expect(new_project_sync_event.reload).to be_persisted
+ end
end
context 'for Namespaces::SyncEvent' do
@@ -88,10 +106,10 @@ RSpec.describe Ci::ProcessSyncEventsService do
it 'consumes events' do
expect { execute }.to change(Namespaces::SyncEvent, :count).from(2).to(0)
- expect(group.ci_namespace_mirror).to have_attributes(
+ expect(group.reload.ci_namespace_mirror).to have_attributes(
traversal_ids: [parent_group_1.id, parent_group_2.id, group.id]
)
- expect(parent_group_2.ci_namespace_mirror).to have_attributes(
+ expect(parent_group_2.reload.ci_namespace_mirror).to have_attributes(
traversal_ids: [parent_group_1.id, parent_group_2.id]
)
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 866015aa523..251159864f5 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -827,11 +827,17 @@ module Ci
end
context 'when project already has running jobs' do
- let!(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
- let!(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+ let(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+ let(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+
+ before do
+ ::Ci::RunningBuild.upsert_shared_runner_build!(build2)
+ ::Ci::RunningBuild.upsert_shared_runner_build!(build3)
+ end
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
+
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_third_job,
@@ -845,6 +851,14 @@ module Ci
shared_examples 'metrics collector' do
it_behaves_like 'attempt counter collector'
it_behaves_like 'jobs queueing time histogram collector'
+
+ context 'when using denormalized data is disabled' do
+ before do
+ stub_feature_flags(ci_pending_builds_maintain_denormalized_data: false)
+ end
+
+ it_behaves_like 'jobs queueing time histogram collector'
+ end
end
context 'when shared runner is used' do
@@ -875,6 +889,16 @@ module Ci
it_behaves_like 'metrics collector'
end
+ context 'when max running jobs bucket size is exceeded' do
+ before do
+ stub_const('Gitlab::Ci::Queue::Metrics::JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET', 1)
+ end
+
+ let(:expected_jobs_running_for_project_third_job) { '1+' }
+
+ it_behaves_like 'metrics collector'
+ end
+
context 'when pending job with queued_at=nil is used' do
before do
pending_job.update!(queued_at: nil)
diff --git a/spec/services/ci/register_runner_service_spec.rb b/spec/services/ci/register_runner_service_spec.rb
new file mode 100644
index 00000000000..e813a1d8b31
--- /dev/null
+++ b/spec/services/ci/register_runner_service_spec.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::RegisterRunnerService do
+ let(:registration_token) { 'abcdefg123456' }
+
+ before do
+ stub_feature_flags(runner_registration_control: false)
+ stub_application_setting(runners_registration_token: registration_token)
+ stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
+ end
+
+ describe '#execute' do
+ let(:token) { }
+ let(:args) { {} }
+
+ subject { described_class.new.execute(token, args) }
+
+ context 'when no token is provided' do
+ let(:token) { '' }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when invalid token is provided' do
+ let(:token) { 'invalid' }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when valid token is provided' do
+ context 'with a registration token' do
+ let(:token) { registration_token }
+
+ it 'creates runner with default values' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.persisted?).to be_truthy
+ expect(subject.run_untagged).to be true
+ expect(subject.active).to be true
+ expect(subject.token).not_to eq(registration_token)
+ expect(subject).to be_instance_type
+ end
+
+ context 'with non-default arguments' do
+ let(:args) do
+ {
+ description: 'some description',
+ active: false,
+ locked: true,
+ run_untagged: false,
+ tag_list: %w(tag1 tag2),
+ access_level: 'ref_protected',
+ maximum_timeout: 600,
+ name: 'some name',
+ version: 'some version',
+ revision: 'some revision',
+ platform: 'some platform',
+ architecture: 'some architecture',
+ ip_address: '10.0.0.1',
+ config: {
+ gpus: 'some gpu config'
+ }
+ }
+ end
+
+ it 'creates runner with specified values', :aggregate_failures do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.active).to eq args[:active]
+ expect(subject.locked).to eq args[:locked]
+ expect(subject.run_untagged).to eq args[:run_untagged]
+ expect(subject.tags).to contain_exactly(
+ an_object_having_attributes(name: 'tag1'),
+ an_object_having_attributes(name: 'tag2')
+ )
+ expect(subject.access_level).to eq args[:access_level]
+ expect(subject.maximum_timeout).to eq args[:maximum_timeout]
+ expect(subject.name).to eq args[:name]
+ expect(subject.version).to eq args[:version]
+ expect(subject.revision).to eq args[:revision]
+ expect(subject.platform).to eq args[:platform]
+ expect(subject.architecture).to eq args[:architecture]
+ expect(subject.ip_address).to eq args[:ip_address]
+ end
+ end
+ end
+
+ context 'when project token is used' do
+ let(:project) { create(:project) }
+ let(:token) { project.runners_token }
+
+ it 'creates project runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(project.runners.size).to eq(1)
+ is_expected.to eq(project.runners.first)
+ expect(subject.token).not_to eq(registration_token)
+ expect(subject.token).not_to eq(project.runners_token)
+ expect(subject).to be_project_type
+ end
+
+ context 'when it exceeds the application limits' do
+ before do
+ create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
+ end
+
+ it 'does not create runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.persisted?).to be_falsey
+ expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded'])
+ expect(project.runners.reload.size).to eq(1)
+ end
+ end
+
+ context 'when abandoned runners cause application limits to not be exceeded' do
+ before do
+ create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
+ end
+
+ it 'creates runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(project.runners.reload.size).to eq(2)
+ expect(project.runners.recent.size).to eq(1)
+ end
+ end
+
+ context 'when valid runner registrars do not include project' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(runner_registration_control: true)
+ end
+
+ it 'returns 403 error' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ it 'registers the runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(subject.active).to be true
+ end
+ end
+ end
+ end
+
+ context 'when group token is used' do
+ let(:group) { create(:group) }
+ let(:token) { group.runners_token }
+
+ it 'creates a group runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(group.runners.reload.size).to eq(1)
+ expect(subject.token).not_to eq(registration_token)
+ expect(subject.token).not_to eq(group.runners_token)
+ expect(subject).to be_group_type
+ end
+
+ context 'when it exceeds the application limits' do
+ before do
+ create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
+ end
+
+ it 'does not create runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.persisted?).to be_falsey
+ expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded'])
+ expect(group.runners.reload.size).to eq(1)
+ end
+ end
+
+ context 'when abandoned runners cause application limits to not be exceeded' do
+ before do
+ create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
+ create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
+ end
+
+ it 'creates runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(group.runners.reload.size).to eq(3)
+ expect(group.runners.recent.size).to eq(1)
+ end
+ end
+
+ context 'when valid runner registrars do not include group' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['project'])
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(runner_registration_control: true)
+ end
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ it 'registers the runner' do
+ is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(subject.errors).to be_empty
+ expect(subject.active).to be true
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 5d56084faa8..4e8e41ca6e6 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -188,13 +188,6 @@ RSpec.describe Ci::RetryBuildService do
expect(new_build).to be_pending
end
- it 'resolves todos for old build that failed' do
- expect(::MergeRequests::AddTodoWhenBuildFailsService)
- .to receive_message_chain(:new, :close)
-
- service.execute(build)
- end
-
context 'when there are subsequent processables that are skipped' do
let!(:subsequent_build) do
create(:ci_build, :skipped, stage_idx: 2,
@@ -272,6 +265,17 @@ RSpec.describe Ci::RetryBuildService do
expect(bridge.reload).to be_pending
end
end
+
+ context 'when there is a failed job todo for the MR' do
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, head_pipeline: pipeline) }
+ let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: user, target: merge_request) }
+
+ it 'resolves the todo for the old failed build' do
+ expect do
+ service.execute(build)
+ end.to change { todo.reload.state }.from('pending').to('done')
+ end
+ end
end
context 'when user does not have ability to execute build' do
@@ -367,6 +371,14 @@ RSpec.describe Ci::RetryBuildService do
it_behaves_like 'when build with dynamic environment is retried'
context 'when create_deployment_in_separate_transaction feature flag is disabled' do
+ let(:new_build) do
+ travel_to(1.second.from_now) do
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345668') do
+ service.clone!(build)
+ end
+ end
+ end
+
before do
stub_feature_flags(create_deployment_in_separate_transaction: false)
end
diff --git a/spec/services/clusters/agent_tokens/track_usage_service_spec.rb b/spec/services/clusters/agent_tokens/track_usage_service_spec.rb
new file mode 100644
index 00000000000..3350b15a5ce
--- /dev/null
+++ b/spec/services/clusters/agent_tokens/track_usage_service_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentTokens::TrackUsageService do
+ let_it_be(:agent) { create(:cluster_agent) }
+
+ describe '#execute', :clean_gitlab_redis_cache do
+ let(:agent_token) { create(:cluster_agent_token, agent: agent) }
+
+ subject { described_class.new(agent_token).execute }
+
+ context 'when last_used_at was updated recently' do
+ before do
+ agent_token.update!(last_used_at: 10.minutes.ago)
+ end
+
+ it 'updates cache but not database' do
+ expect { subject }.not_to change { agent_token.reload.read_attribute(:last_used_at) }
+
+ expect_redis_update
+ end
+ end
+
+ context 'when last_used_at was not updated recently' do
+ it 'updates cache and database' do
+ does_db_update
+ expect_redis_update
+ end
+
+ context 'with invalid token' do
+ before do
+ agent_token.description = SecureRandom.hex(2000)
+ end
+
+ it 'still updates caches and database' do
+ expect(agent_token).to be_invalid
+
+ does_db_update
+ expect_redis_update
+ end
+ end
+
+ context 'agent is not connected' do
+ before do
+ allow(agent).to receive(:connected?).and_return(false)
+ end
+
+ it 'creates an activity event' do
+ expect { subject }.to change { agent.activity_events.count }
+
+ event = agent.activity_events.last
+ expect(event).to have_attributes(
+ kind: 'agent_connected',
+ level: 'info',
+ recorded_at: agent_token.reload.read_attribute(:last_used_at),
+ agent_token: agent_token
+ )
+ end
+ end
+
+ context 'agent is connected' do
+ before do
+ allow(agent).to receive(:connected?).and_return(true)
+ end
+
+ it 'does not create an activity event' do
+ expect { subject }.not_to change { agent.activity_events.count }
+ end
+ end
+ end
+
+ def expect_redis_update
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = "cache:#{agent_token.class}:#{agent_token.id}:attributes"
+ expect(redis.get(redis_key)).to be_present
+ end
+ end
+
+ def does_db_update
+ expect { subject }.to change { agent_token.reload.read_attribute(:last_used_at) }
+ end
+ end
+end
diff --git a/spec/services/clusters/agents/create_activity_event_service_spec.rb b/spec/services/clusters/agents/create_activity_event_service_spec.rb
new file mode 100644
index 00000000000..7a8f0e16d60
--- /dev/null
+++ b/spec/services/clusters/agents/create_activity_event_service_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::CreateActivityEventService do
+ let_it_be(:agent) { create(:cluster_agent) }
+ let_it_be(:token) { create(:cluster_agent_token, agent: agent) }
+ let_it_be(:user) { create(:user) }
+
+ describe '#execute' do
+ let(:params) do
+ {
+ kind: :token_created,
+ level: :info,
+ recorded_at: token.created_at,
+ user: user,
+ agent_token: token
+ }
+ end
+
+ subject { described_class.new(agent, **params).execute }
+
+ it 'creates an activity event record' do
+ expect { subject }.to change(agent.activity_events, :count).from(0).to(1)
+
+ event = agent.activity_events.last
+
+ expect(event).to have_attributes(
+ kind: 'token_created',
+ level: 'info',
+ recorded_at: token.reload.created_at,
+ user: user,
+ agent_token_id: token.id
+ )
+ end
+
+ it 'schedules the cleanup worker' do
+ expect(Clusters::Agents::DeleteExpiredEventsWorker).to receive(:perform_at)
+ .with(1.hour.from_now.change(min: agent.id % 60), agent.id)
+
+ subject
+ end
+ end
+end
diff --git a/spec/services/clusters/agents/delete_expired_events_service_spec.rb b/spec/services/clusters/agents/delete_expired_events_service_spec.rb
new file mode 100644
index 00000000000..3dc166f54eb
--- /dev/null
+++ b/spec/services/clusters/agents/delete_expired_events_service_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::DeleteExpiredEventsService do
+ let_it_be(:agent) { create(:cluster_agent) }
+
+ describe '#execute' do
+ let_it_be(:event1) { create(:agent_activity_event, agent: agent, recorded_at: 1.hour.ago) }
+ let_it_be(:event2) { create(:agent_activity_event, agent: agent, recorded_at: 2.hours.ago) }
+ let_it_be(:event3) { create(:agent_activity_event, agent: agent, recorded_at: 3.hours.ago) }
+ let_it_be(:event4) { create(:agent_activity_event, agent: agent, recorded_at: 4.hours.ago) }
+ let_it_be(:event5) { create(:agent_activity_event, agent: agent, recorded_at: 5.hours.ago) }
+
+ let(:deletion_cutoff) { 1.day.ago }
+
+ subject { described_class.new(agent).execute }
+
+ before do
+ allow(agent).to receive(:activity_event_deletion_cutoff).and_return(deletion_cutoff)
+ end
+
+ it 'does not delete events if the limit has not been reached' do
+ expect { subject }.not_to change(agent.activity_events, :count)
+ end
+
+ context 'there are more events than the limit' do
+ let(:deletion_cutoff) { event3.recorded_at }
+
+ it 'removes events to remain at the limit, keeping the most recent' do
+ expect { subject }.to change(agent.activity_events, :count).from(5).to(3)
+ expect(agent.activity_events).to contain_exactly(event1, event2, event3)
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/integrations/create_service_spec.rb b/spec/services/clusters/integrations/create_service_spec.rb
index 14653236ab1..6dac97ebf8f 100644
--- a/spec/services/clusters/integrations/create_service_spec.rb
+++ b/spec/services/clusters/integrations/create_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Clusters::Integrations::CreateService, '#execute' do
let_it_be_with_reload(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:service) do
- described_class.new(container: project, cluster: cluster, current_user: project.owner, params: params)
+ described_class.new(container: project, cluster: cluster, current_user: project.first_owner, params: params)
end
shared_examples_for 'a cluster integration' do |application_type|
diff --git a/spec/services/customer_relations/contacts/create_service_spec.rb b/spec/services/customer_relations/contacts/create_service_spec.rb
index 71eb447055e..567e1c91e78 100644
--- a/spec/services/customer_relations/contacts/create_service_spec.rb
+++ b/spec/services/customer_relations/contacts/create_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe CustomerRelations::Contacts::CreateService do
subject(:response) { described_class.new(group: group, current_user: user, params: params).execute }
context 'when user does not have permission' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_reporter(user)
@@ -25,7 +25,7 @@ RSpec.describe CustomerRelations::Contacts::CreateService do
end
context 'when user has permission' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_developer(user)
diff --git a/spec/services/customer_relations/contacts/update_service_spec.rb b/spec/services/customer_relations/contacts/update_service_spec.rb
index 7c5fbabb600..253bbc23226 100644
--- a/spec/services/customer_relations/contacts/update_service_spec.rb
+++ b/spec/services/customer_relations/contacts/update_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe CustomerRelations::Contacts::UpdateService do
describe '#execute' do
context 'when the user has no permission' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let(:params) { { first_name: 'Gary' } }
@@ -24,7 +24,7 @@ RSpec.describe CustomerRelations::Contacts::UpdateService do
end
context 'when user has permission' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_developer(user)
diff --git a/spec/services/customer_relations/organizations/create_service_spec.rb b/spec/services/customer_relations/organizations/create_service_spec.rb
index d8985d8d90b..18eefdd716e 100644
--- a/spec/services/customer_relations/organizations/create_service_spec.rb
+++ b/spec/services/customer_relations/organizations/create_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe CustomerRelations::Organizations::CreateService do
describe '#execute' do
let_it_be(:user) { create(:user) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :crm_enabled) }
let(:params) { attributes_for(:organization, group: group) }
subject(:response) { described_class.new(group: group, current_user: user, params: params).execute }
diff --git a/spec/services/customer_relations/organizations/update_service_spec.rb b/spec/services/customer_relations/organizations/update_service_spec.rb
index bc40cb3e8e7..8461c98ef0e 100644
--- a/spec/services/customer_relations/organizations/update_service_spec.rb
+++ b/spec/services/customer_relations/organizations/update_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe CustomerRelations::Organizations::UpdateService do
describe '#execute' do
context 'when the user has no permission' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let(:params) { { name: 'GitLab' } }
@@ -24,7 +24,7 @@ RSpec.describe CustomerRelations::Organizations::UpdateService do
end
context 'when user has permission' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
before_all do
group.add_developer(user)
diff --git a/spec/services/dependency_proxy/download_blob_service_spec.rb b/spec/services/dependency_proxy/download_blob_service_spec.rb
deleted file mode 100644
index 2f293b8a46b..00000000000
--- a/spec/services/dependency_proxy/download_blob_service_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe DependencyProxy::DownloadBlobService do
- include DependencyProxyHelpers
-
- let(:image) { 'alpine' }
- let(:token) { Digest::SHA256.hexdigest('123') }
- let(:blob_sha) { Digest::SHA256.hexdigest('ruby:2.7.0') }
-
- subject(:download_blob) { described_class.new(image, blob_sha, token).execute }
-
- context 'remote request is successful' do
- before do
- stub_blob_download(image, blob_sha)
- end
-
- it { expect(subject[:status]).to eq(:success) }
- it { expect(subject[:file]).to be_a(Tempfile) }
- it { expect(subject[:file].size).to eq(6) }
-
- it 'streams the download' do
- expected_options = { headers: anything, stream_body: true }
-
- expect(Gitlab::HTTP).to receive(:perform_request).with(Net::HTTP::Get, anything, expected_options)
-
- download_blob
- end
-
- it 'skips read_total_timeout', :aggregate_failures do
- stub_const('GitLab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT', 0)
-
- expect(Gitlab::Metrics::System).not_to receive(:monotonic_time)
- expect(download_blob).to include(status: :success)
- end
- end
-
- context 'remote request is not found' do
- before do
- stub_blob_download(image, blob_sha, 404)
- end
-
- it { expect(subject[:status]).to eq(:error) }
- it { expect(subject[:http_status]).to eq(404) }
- it { expect(subject[:message]).to eq('Non-success response code on downloading blob fragment') }
- end
-
- context 'net timeout exception' do
- before do
- blob_url = DependencyProxy::Registry.blob_url(image, blob_sha)
-
- stub_full_request(blob_url).to_timeout
- end
-
- it { expect(subject[:status]).to eq(:error) }
- it { expect(subject[:http_status]).to eq(599) }
- it { expect(subject[:message]).to eq('execution expired') }
- end
-end
diff --git a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
index 29bdf1f11c3..607d67d8efe 100644
--- a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
@@ -91,9 +91,9 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
it_behaves_like 'returning no manifest'
end
- context 'when the cached manifest is expired' do
+ context 'when the cached manifest is pending destruction' do
before do
- dependency_proxy_manifest.update_column(:status, DependencyProxy::Manifest.statuses[:expired])
+ dependency_proxy_manifest.update_column(:status, DependencyProxy::Manifest.statuses[:pending_destruction])
stub_manifest_head(image, tag, headers: headers)
stub_manifest_download(image, tag, headers: headers)
end
diff --git a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
deleted file mode 100644
index 5f7afdf699a..00000000000
--- a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe DependencyProxy::FindOrCreateBlobService do
- include DependencyProxyHelpers
-
- let_it_be_with_reload(:blob) { create(:dependency_proxy_blob) }
-
- let(:group) { blob.group }
- let(:image) { 'alpine' }
- let(:tag) { '3.9' }
- let(:token) { Digest::SHA256.hexdigest('123') }
- let(:blob_sha) { '40bd001563085fc35165329ea1ff5c5ecbdbbeef' }
-
- subject { described_class.new(group, image, token, blob_sha).execute }
-
- before do
- stub_registry_auth(image, token)
- end
-
- shared_examples 'downloads the remote blob' do
- it 'downloads blob from remote registry if there is no cached one' do
- expect(subject[:status]).to eq(:success)
- expect(subject[:blob]).to be_a(DependencyProxy::Blob)
- expect(subject[:blob]).to be_persisted
- expect(subject[:from_cache]).to eq false
- end
- end
-
- context 'no cache' do
- before do
- stub_blob_download(image, blob_sha)
- end
-
- it_behaves_like 'downloads the remote blob'
- end
-
- context 'cached blob' do
- let(:blob_sha) { blob.file_name.sub('.gz', '') }
-
- it 'uses cached blob instead of downloading one' do
- expect { subject }.to change { blob.reload.read_at }
-
- expect(subject[:status]).to eq(:success)
- expect(subject[:blob]).to be_a(DependencyProxy::Blob)
- expect(subject[:blob]).to eq(blob)
- expect(subject[:from_cache]).to eq true
- end
-
- context 'when the cached blob is expired' do
- before do
- blob.update_column(:status, DependencyProxy::Blob.statuses[:expired])
- stub_blob_download(image, blob_sha)
- end
-
- it_behaves_like 'downloads the remote blob'
- end
- end
-
- context 'no such blob exists remotely' do
- before do
- stub_blob_download(image, blob_sha, 404)
- end
-
- it 'returns error message and http status' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq('Failed to download the blob')
- expect(subject[:http_status]).to eq(404)
- end
- end
-end
diff --git a/spec/services/deployments/archive_in_project_service_spec.rb b/spec/services/deployments/archive_in_project_service_spec.rb
index d4039ee7b4a..a316c210d64 100644
--- a/spec/services/deployments/archive_in_project_service_spec.rb
+++ b/spec/services/deployments/archive_in_project_service_spec.rb
@@ -50,17 +50,6 @@ RSpec.describe Deployments::ArchiveInProjectService do
end
end
- context 'when deployments_archive feature flag is disabled' do
- before do
- stub_feature_flags(deployments_archive: false)
- end
-
- it 'does not do anything' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq('Feature flag is not enabled')
- end
- end
-
def deployment_refs_exist?
deployment_refs.map { |path| project.repository.ref_exists?(path) }
end
diff --git a/spec/services/deployments/create_for_build_service_spec.rb b/spec/services/deployments/create_for_build_service_spec.rb
new file mode 100644
index 00000000000..6fc7c9e56a6
--- /dev/null
+++ b/spec/services/deployments/create_for_build_service_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Deployments::CreateForBuildService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new }
+
+ describe '#execute' do
+ subject { service.execute(build) }
+
+ context 'with a deployment job' do
+ let!(:build) { create(:ci_build, :start_review_app, project: project) }
+ let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
+
+ it 'creates a deployment record' do
+ expect { subject }.to change { Deployment.count }.by(1)
+
+ build.reset
+ expect(build.deployment.project).to eq(build.project)
+ expect(build.deployment.ref).to eq(build.ref)
+ expect(build.deployment.sha).to eq(build.sha)
+ expect(build.deployment.deployable).to eq(build)
+ expect(build.deployment.deployable_type).to eq('CommitStatus')
+ expect(build.deployment.environment).to eq(build.persisted_environment)
+ end
+
+ context 'when creation failure occures' do
+ before do
+ allow(build).to receive(:create_deployment!) { raise ActiveRecord::RecordInvalid }
+ end
+
+ it 'trackes the exception' do
+ expect { subject }.to raise_error(described_class::DeploymentCreationError)
+
+ expect(Deployment.count).to eq(0)
+ end
+ end
+
+ context 'when the corresponding environment does not exist' do
+ let!(:environment) { }
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+
+ expect(build.deployment).to be_nil
+ end
+ end
+ end
+
+ context 'with a teardown job' do
+ let!(:build) { create(:ci_build, :stop_review_app, project: project) }
+ let!(:environment) { create(:environment, name: build.expanded_environment_name) }
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+
+ expect(build.deployment).to be_nil
+ end
+ end
+
+ context 'with a normal job' do
+ let!(:build) { create(:ci_build, project: project) }
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+
+ expect(build.deployment).to be_nil
+ end
+ end
+
+ context 'with a bridge' do
+ let!(:build) { create(:ci_bridge, project: project) }
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index 85020e95c83..e7a3505bbd4 100644
--- a/spec/services/discussions/update_diff_position_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Discussions::UpdateDiffPositionService do
let(:project) { create(:project, :repository) }
- let(:current_user) { project.owner }
+ let(:current_user) { project.first_owner }
let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
diff --git a/spec/services/error_tracking/collect_error_service_spec.rb b/spec/services/error_tracking/collect_error_service_spec.rb
index 52d095148c8..2b16612dac3 100644
--- a/spec/services/error_tracking/collect_error_service_spec.rb
+++ b/spec/services/error_tracking/collect_error_service_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe ErrorTracking::CollectErrorService do
let_it_be(:project) { create(:project) }
- let_it_be(:parsed_event_file) { 'error_tracking/parsed_event.json' }
- let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file(parsed_event_file)) }
+
+ let(:parsed_event_file) { 'error_tracking/parsed_event.json' }
+ let(:parsed_event) { parse_valid_event(parsed_event_file) }
subject { described_class.new(project, nil, event: parsed_event) }
@@ -43,7 +44,7 @@ RSpec.describe ErrorTracking::CollectErrorService do
end
context 'python sdk event' do
- let(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/python_event.json')) }
+ let(:parsed_event_file) { 'error_tracking/python_event.json' }
it 'creates a valid event' do
expect { subject.execute }.to change { ErrorTracking::ErrorEvent.count }.by(1)
@@ -75,7 +76,7 @@ RSpec.describe ErrorTracking::CollectErrorService do
end
context 'go payload' do
- let(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/go_parsed_event.json')) }
+ let(:parsed_event_file) { 'error_tracking/go_parsed_event.json' }
it 'has correct values set' do
subject.execute
@@ -92,6 +93,38 @@ RSpec.describe ErrorTracking::CollectErrorService do
expect(event.environment).to eq 'Accumulate'
expect(event.payload).to eq parsed_event
end
+
+ context 'with two exceptions' do
+ let(:parsed_event_file) { 'error_tracking/go_two_exception_event.json' }
+
+ it 'reports using second exception', :aggregate_failures do
+ subject.execute
+
+ event = ErrorTracking::ErrorEvent.last
+ error = event.error
+
+ expect(error.name).to eq '*url.Error'
+ expect(error.description).to eq(%(Get \"foobar\": unsupported protocol scheme \"\"))
+ expect(error.platform).to eq 'go'
+ expect(error.actor).to eq('main(main)')
+
+ expect(event.description).to eq(%(Get \"foobar\": unsupported protocol scheme \"\"))
+ expect(event.payload).to eq parsed_event
+ end
+ end
end
end
+
+ private
+
+ def parse_valid_event(parsed_event_file)
+ parsed_event = Gitlab::Json.parse(fixture_file(parsed_event_file))
+
+ validator = ErrorTracking::Collector::PayloadValidator.new
+ # This a precondition for all specs to verify that
+ # submitted JSON payload is valid.
+ expect(validator).to be_valid(parsed_event)
+
+ parsed_event
+ end
end
diff --git a/spec/services/events/destroy_service_spec.rb b/spec/services/events/destroy_service_spec.rb
index 8dcbb83eb1d..8b07852c040 100644
--- a/spec/services/events/destroy_service_spec.rb
+++ b/spec/services/events/destroy_service_spec.rb
@@ -30,16 +30,28 @@ RSpec.describe Events::DestroyService do
expect(unrelated_event.reload).to be_present
end
+ context 'batch delete' do
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+ end
+
+ it 'splits delete queries into batches' do
+ expect(project).to receive(:events).twice.and_call_original
+
+ subject.execute
+ end
+ end
+
context 'when an error is raised while deleting the records' do
before do
- allow(project).to receive_message_chain(:events, :all, :delete_all).and_raise(ActiveRecord::ActiveRecordError)
+ allow(project).to receive_message_chain(:events, :limit, :delete_all).and_raise(ActiveRecord::ActiveRecordError, 'custom error')
end
it 'returns error' do
response = subject.execute
expect(response).to be_error
- expect(response.message).to eq 'Failed to remove events.'
+ expect(response.message).to eq 'custom error'
end
it 'does not delete events' do
diff --git a/spec/services/feature_flags/hook_service_spec.rb b/spec/services/feature_flags/hook_service_spec.rb
index 02cdbbd86ac..19c935e43f3 100644
--- a/spec/services/feature_flags/hook_service_spec.rb
+++ b/spec/services/feature_flags/hook_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe FeatureFlags::HookService do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
- let_it_be(:user) { namespace.owner }
+ let_it_be(:user) { namespace.first_owner }
let!(:hook) { create(:project_hook, project: project) }
let(:hook_data) { double }
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index f52df9b0073..05c1f898cab 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Git::ProcessRefChangesService do
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:params) { { changes: git_changes } }
subject { described_class.new(project, user, params) }
@@ -34,7 +34,7 @@ RSpec.describe Git::ProcessRefChangesService do
it "calls #{push_service_class}" do
expect(push_service_class)
.to receive(:new)
- .with(project, project.owner, hash_including(execute_project_hooks: true, create_push_event: true))
+ .with(project, project.first_owner, hash_including(execute_project_hooks: true, create_push_event: true))
.exactly(changes.count).times
.and_return(service)
@@ -58,7 +58,7 @@ RSpec.describe Git::ProcessRefChangesService do
it "calls #{push_service_class} with execute_project_hooks set to false" do
expect(push_service_class)
.to receive(:new)
- .with(project, project.owner, hash_including(execute_project_hooks: false))
+ .with(project, project.first_owner, hash_including(execute_project_hooks: false))
.exactly(changes.count).times
.and_return(service)
@@ -86,7 +86,7 @@ RSpec.describe Git::ProcessRefChangesService do
it "calls #{push_service_class} with create_push_event set to false" do
expect(push_service_class)
.to receive(:new)
- .with(project, project.owner, hash_including(create_push_event: false))
+ .with(project, project.first_owner, hash_including(create_push_event: false))
.exactly(changes.count).times
.and_return(service)
@@ -170,7 +170,7 @@ RSpec.describe Git::ProcessRefChangesService do
allow(push_service_class)
.to receive(:new)
- .with(project, project.owner, hash_including(execute_project_hooks: true, create_push_event: true))
+ .with(project, project.first_owner, hash_including(execute_project_hooks: true, create_push_event: true))
.exactly(changes.count).times
.and_return(service)
end
diff --git a/spec/services/google_cloud/create_service_accounts_service_spec.rb b/spec/services/google_cloud/create_service_accounts_service_spec.rb
new file mode 100644
index 00000000000..190e1a8098c
--- /dev/null
+++ b/spec/services/google_cloud/create_service_accounts_service_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Mock Types
+MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
+MockServiceAccount = Struct.new(:project_id, :unique_id)
+
+RSpec.describe GoogleCloud::CreateServiceAccountsService do
+ describe '#execute' do
+ before do
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
+ .with('google_oauth2')
+ .and_return(MockGoogleOAuth2Credentials.new('mock-app-id', 'mock-app-secret'))
+
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:create_service_account)
+ .and_return(MockServiceAccount.new('mock-project-id', 'mock-unique-id'))
+ allow(client).to receive(:create_service_account_key)
+ .and_return('mock-key')
+ end
+ end
+
+ it 'creates unprotected vars', :aggregate_failures do
+ project = create(:project)
+
+ service = described_class.new(
+ project,
+ nil,
+ google_oauth2_token: 'mock-token',
+ gcp_project_id: 'mock-gcp-project-id',
+ environment_name: '*'
+ )
+
+ response = service.execute
+
+ expect(response.status).to eq(:success)
+ expect(response.message).to eq('Service account generated successfully')
+ expect(project.variables.count).to eq(3)
+ expect(project.variables.first.protected).to eq(false)
+ expect(project.variables.second.protected).to eq(false)
+ expect(project.variables.third.protected).to eq(false)
+ end
+ end
+end
diff --git a/spec/services/google_cloud/service_accounts_service_spec.rb b/spec/services/google_cloud/service_accounts_service_spec.rb
index 505c623c02a..17c1f61a96e 100644
--- a/spec/services/google_cloud/service_accounts_service_spec.rb
+++ b/spec/services/google_cloud/service_accounts_service_spec.rb
@@ -60,8 +60,8 @@ RSpec.describe GoogleCloud::ServiceAccountsService do
let_it_be(:project) { create(:project) }
it 'saves GCP creds as project CI vars' do
- service.add_for_project('env_1', 'gcp_prj_id_1', 'srv_acc_1', 'srv_acc_key_1')
- service.add_for_project('env_2', 'gcp_prj_id_2', 'srv_acc_2', 'srv_acc_key_2')
+ service.add_for_project('env_1', 'gcp_prj_id_1', 'srv_acc_1', 'srv_acc_key_1', true)
+ service.add_for_project('env_2', 'gcp_prj_id_2', 'srv_acc_2', 'srv_acc_key_2', false)
list = service.find_for_project
@@ -81,7 +81,7 @@ RSpec.describe GoogleCloud::ServiceAccountsService do
end
it 'replaces previously stored CI vars with new CI vars' do
- service.add_for_project('env_1', 'new_project', 'srv_acc_1', 'srv_acc_key_1')
+ service.add_for_project('env_1', 'new_project', 'srv_acc_1', 'srv_acc_key_1', false)
list = service.find_for_project
@@ -101,9 +101,16 @@ RSpec.describe GoogleCloud::ServiceAccountsService do
end
end
- it 'underlying project CI vars must be protected' do
- expect(project.variables.first.protected).to eq(true)
- expect(project.variables.second.protected).to eq(true)
+ it 'underlying project CI vars must be protected as per value' do
+ service.add_for_project('env_1', 'gcp_prj_id_1', 'srv_acc_1', 'srv_acc_key_1', true)
+ service.add_for_project('env_2', 'gcp_prj_id_2', 'srv_acc_2', 'srv_acc_key_2', false)
+
+ expect(project.variables[0].protected).to eq(true)
+ expect(project.variables[1].protected).to eq(true)
+ expect(project.variables[2].protected).to eq(true)
+ expect(project.variables[3].protected).to eq(false)
+ expect(project.variables[4].protected).to eq(false)
+ expect(project.variables[5].protected).to eq(false)
end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index e1bd3732820..46c5e2a9818 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -163,6 +163,70 @@ RSpec.describe Groups::UpdateService do
expect(updated_group.parent_id).to be_nil
end
end
+
+ context 'crm_enabled param' do
+ context 'when no existing crm_settings' do
+ it 'when param not present, leave crm disabled' do
+ params = {}
+
+ described_class.new(public_group, user, params).execute
+ updated_group = public_group.reload
+
+ expect(updated_group.crm_enabled?).to be_falsey
+ end
+
+ it 'when param set true, enables crm' do
+ params = { crm_enabled: true }
+
+ described_class.new(public_group, user, params).execute
+ updated_group = public_group.reload
+
+ expect(updated_group.crm_enabled?).to be_truthy
+ end
+ end
+
+ context 'with existing crm_settings' do
+ it 'when param set true, enables crm' do
+ params = { crm_enabled: true }
+ create(:crm_settings, group: public_group)
+
+ described_class.new(public_group, user, params).execute
+
+ updated_group = public_group.reload
+ expect(updated_group.crm_enabled?).to be_truthy
+ end
+
+ it 'when param set false, disables crm' do
+ params = { crm_enabled: false }
+ create(:crm_settings, group: public_group, enabled: true)
+
+ described_class.new(public_group, user, params).execute
+
+ updated_group = public_group.reload
+ expect(updated_group.crm_enabled?).to be_falsy
+ end
+
+ it 'when param not present, crm remains disabled' do
+ params = {}
+ create(:crm_settings, group: public_group)
+
+ described_class.new(public_group, user, params).execute
+
+ updated_group = public_group.reload
+ expect(updated_group.crm_enabled?).to be_falsy
+ end
+
+ it 'when param not present, crm remains enabled' do
+ params = {}
+ create(:crm_settings, group: public_group, enabled: true)
+
+ described_class.new(public_group, user, params).execute
+
+ updated_group = public_group.reload
+ expect(updated_group.crm_enabled?).to be_truthy
+ end
+ end
+ end
end
context "unauthorized visibility_level validation" do
diff --git a/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb b/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb
index 3c461c91ff0..92c46cf7052 100644
--- a/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb
+++ b/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb
@@ -18,24 +18,29 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do
subject { described_class.new(user, params) }
- it 'creates a project and returns a successful response' do
- stub_headers_for(remote_url, {
- 'content-type' => 'application/gzip',
- 'content-length' => '10'
- })
-
- response = nil
- expect { response = subject.execute }
- .to change(Project, :count).by(1)
+ shared_examples 'successfully import' do |content_type|
+ it 'creates a project and returns a successful response' do
+ stub_headers_for(remote_url, {
+ 'content-type' => content_type,
+ 'content-length' => '10'
+ })
- expect(response).to be_success
- expect(response.http_status).to eq(:ok)
- expect(response.payload).to be_instance_of(Project)
- expect(response.payload.name).to eq('name')
- expect(response.payload.path).to eq('path')
- expect(response.payload.namespace).to eq(user.namespace)
+ response = nil
+ expect { response = subject.execute }
+ .to change(Project, :count).by(1)
+
+ expect(response).to be_success
+ expect(response.http_status).to eq(:ok)
+ expect(response.payload).to be_instance_of(Project)
+ expect(response.payload.name).to eq('name')
+ expect(response.payload.path).to eq('path')
+ expect(response.payload.namespace).to eq(user.namespace)
+ end
end
+ it_behaves_like 'successfully import', 'application/gzip'
+ it_behaves_like 'successfully import', 'application/x-tar'
+
context 'when the file url is invalid' do
it 'returns an erred response with the reason of the failure' do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
@@ -79,7 +84,7 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do
expect(response).not_to be_success
expect(response.http_status).to eq(:bad_request)
expect(response.message)
- .to eq("Remote file content type 'application/js' not allowed. (Allowed content types: application/gzip)")
+ .to eq("Remote file content type 'application/js' not allowed. (Allowed content types: application/gzip, application/x-tar)")
end
end
@@ -130,6 +135,20 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do
end
end
+ it 'does not validate content-type or content-length when the file is stored in AWS-S3' do
+ stub_headers_for(remote_url, {
+ 'Server' => 'AmazonS3',
+ 'x-amz-request-id' => 'Something'
+ })
+
+ response = nil
+ expect { response = subject.execute }
+ .to change(Project, :count)
+
+ expect(response).to be_success
+ expect(response.http_status).to eq(:ok)
+ end
+
context 'when required parameters are not provided' do
let(:params) { {} }
diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
index fbd8a3cb323..9dc862b6ca3 100644
--- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb
+++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
@@ -24,6 +24,17 @@ RSpec.describe Import::ValidateRemoteGitEndpointService do
expect(Gitlab::HTTP).to have_received(:get).with(endpoint_url, basic_auth: nil, stream_body: true, follow_redirects: false)
end
+ context 'when uri is using git:// protocol' do
+ subject { described_class.new(url: 'git://demo.host/repo')}
+
+ it 'returns success' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ end
+ end
+
context 'when receiving HTTP response' do
subject { described_class.new(url: base_url) }
diff --git a/spec/services/incident_management/incidents/create_service_spec.rb b/spec/services/incident_management/incidents/create_service_spec.rb
index 0f32a4b5425..47ce9d01f66 100644
--- a/spec/services/incident_management/incidents/create_service_spec.rb
+++ b/spec/services/incident_management/incidents/create_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
let(:issue) { new_issue }
- include_examples 'has incident label'
+ include_examples 'does not have incident label'
end
context 'with default severity' do
@@ -71,8 +71,8 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
end
context 'when incident label does not exists' do
- it 'creates incident label' do
- expect { create_incident }.to change { project.labels.where(title: label_title).count }.by(1)
+ it 'does not create incident label' do
+ expect { create_incident }.to not_change { project.labels.where(title: label_title).count }
end
end
diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
new file mode 100644
index 00000000000..e9db6ba8d28
--- /dev/null
+++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) }
+ let_it_be(:issue, reload: true) { escalation_status.issue }
+ let_it_be(:project) { issue.project }
+ let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) }
+
+ let(:status_event) { :acknowledge }
+ let(:update_params) { { incident_management_issuable_escalation_status_attributes: { status_event: status_event } } }
+ let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) }
+
+ subject(:result) do
+ issue.update!(update_params)
+ service.execute
+ end
+
+ before do
+ issue.project.add_developer(current_user)
+ end
+
+ shared_examples 'does not attempt to update the alert' do
+ specify do
+ expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
+
+ expect(result).to be_success
+ end
+ end
+
+ context 'with status attributes' do
+ it 'updates the alert with the new alert status' do
+ expect(::AlertManagement::Alerts::UpdateService).to receive(:new).once.and_call_original
+ expect(described_class).to receive(:new).once.and_call_original
+
+ expect { result }.to change { escalation_status.reload.acknowledged? }.to(true)
+ .and change { alert.reload.acknowledged? }.to(true)
+ end
+
+ context 'when incident is not associated with an alert' do
+ before do
+ alert.destroy!
+ end
+
+ it_behaves_like 'does not attempt to update the alert'
+ end
+
+ context 'when new status matches the current status' do
+ let(:status_event) { :trigger }
+
+ it_behaves_like 'does not attempt to update the alert'
+ end
+ end
+end
diff --git a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
new file mode 100644
index 00000000000..bfed5319028
--- /dev/null
+++ b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateService do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, :triggered) }
+ let_it_be(:user_with_permissions) { create(:user) }
+
+ let(:current_user) { user_with_permissions }
+ let(:issue) { escalation_status.issue }
+ let(:status) { :acknowledged }
+ let(:params) { { status: status } }
+ let(:service) { IncidentManagement::IssuableEscalationStatuses::PrepareUpdateService.new(issue, current_user, params) }
+
+ subject(:result) { service.execute }
+
+ before do
+ issue.project.add_developer(user_with_permissions)
+ end
+
+ shared_examples 'successful response' do |payload|
+ it 'returns valid parameters which can be used to update the issue' do
+ expect(result).to be_success
+ expect(result.payload).to eq(escalation_status: payload)
+ end
+ end
+
+ shared_examples 'error response' do |message|
+ specify do
+ expect(result).to be_error
+ expect(result.message).to eq(message)
+ end
+ end
+
+ shared_examples 'availability error response' do
+ include_examples 'error response', 'Escalation status updates are not available for this issue, user, or project.'
+ end
+
+ shared_examples 'invalid params error response' do
+ include_examples 'error response', 'Invalid value was provided for a parameter.'
+ end
+
+ it_behaves_like 'successful response', { status_event: :acknowledge }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it_behaves_like 'availability error response'
+ end
+
+ context 'when user is anonymous' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'availability error response'
+ end
+
+ context 'when user does not have permissions' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'availability error response'
+ end
+
+ context 'when called with an unsupported issue type' do
+ let(:issue) { create(:issue) }
+
+ it_behaves_like 'availability error response'
+ end
+
+ context 'when an IssuableEscalationStatus record for the issue does not exist' do
+ let(:issue) { create(:incident) }
+
+ it_behaves_like 'availability error response'
+ end
+
+ context 'when called without params' do
+ let(:params) { nil }
+
+ it_behaves_like 'successful response', {}
+ end
+
+ context 'when called with unsupported params' do
+ let(:params) { { escalations_started_at: Time.current } }
+
+ it_behaves_like 'successful response', {}
+ end
+
+ context 'with status param' do
+ context 'when status matches the current status' do
+ let(:params) { { status: :triggered } }
+
+ it_behaves_like 'successful response', {}
+ end
+
+ context 'when status is unsupported' do
+ let(:params) { { status: :mitigated } }
+
+ it_behaves_like 'invalid params error response'
+ end
+
+ context 'when status is a String' do
+ let(:params) { { status: 'acknowledged' } }
+
+ it_behaves_like 'successful response', { status_event: :acknowledge }
+ end
+ end
+end
diff --git a/spec/services/integrations/test/project_service_spec.rb b/spec/services/integrations/test/project_service_spec.rb
index 32f9f632d7a..74833686283 100644
--- a/spec/services/integrations/test/project_service_spec.rb
+++ b/spec/services/integrations/test/project_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Integrations::Test::ProjectService do
let_it_be(:project) { create(:project) }
let(:integration) { create(:integrations_slack, project: project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:event) { nil }
let(:sample_data) { { data: 'sample' } }
let(:success_result) { { success: true, result: {} } }
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index cf75efb5c57..304e4bb3ebb 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -172,9 +172,9 @@ RSpec.describe Issues::BuildService do
end
describe 'setting issue type' do
- context 'with a corresponding WorkItem::Type' do
- let_it_be(:type_issue_id) { WorkItem::Type.default_issue_type.id }
- let_it_be(:type_incident_id) { WorkItem::Type.default_by_type(:incident).id }
+ context 'with a corresponding WorkItems::Type' do
+ let_it_be(:type_issue_id) { WorkItems::Type.default_issue_type.id }
+ let_it_be(:type_incident_id) { WorkItems::Type.default_by_type(:incident).id }
where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
nil | ref(:guest) | ref(:type_issue_id) | 'issue'
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 8496bd31e00..b2dcfb5c6d3 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Issues::CreateService do
include AfterNextHelpers
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
@@ -61,6 +61,7 @@ RSpec.describe Issues::CreateService do
expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
expect(issue).to be_persisted
+ expect(issue).to be_a(::Issue)
expect(issue.title).to eq('Awesome issue')
expect(issue.assignees).to eq([assignee])
expect(issue.labels).to match_array(labels)
@@ -69,6 +70,18 @@ RSpec.describe Issues::CreateService do
expect(issue.work_item_type.base_type).to eq('issue')
end
+ context 'when a build_service is provided' do
+ let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
+
+ let(:issue_from_builder) { WorkItem.new(project: project, title: 'Issue from builder') }
+ let(:build_service) { double(:build_service, execute: issue_from_builder) }
+
+ it 'uses the provided service to build the issue' do
+ expect(issue).to be_persisted
+ expect(issue).to be_a(WorkItem)
+ end
+ end
+
context 'when skip_system_notes is true' do
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
@@ -101,11 +114,11 @@ RSpec.describe Issues::CreateService do
end
it_behaves_like 'incident issue'
- it_behaves_like 'has incident label'
+ it_behaves_like 'does not have incident label'
- it 'does create an incident label' do
+ it 'does not create an incident label' do
expect { subject }
- .to change { Label.where(incident_label_attributes).count }.by(1)
+ .to not_change { Label.where(incident_label_attributes).count }
end
it 'calls IncidentManagement::Incidents::CreateEscalationStatusService' do
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 36af38aef18..ef501f47f0d 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -259,6 +259,16 @@ RSpec.describe Issues::MoveService do
it_behaves_like 'copy or reset relative position'
end
+
+ context 'issue with escalation status' do
+ it 'keeps the escalation status' do
+ escalation_status = create(:incident_management_issuable_escalation_status, issue: old_issue)
+
+ move_service.execute(old_issue, new_project)
+
+ expect(escalation_status.reload.issue).to eq(old_issue)
+ end
+ end
end
describe 'move permissions' do
diff --git a/spec/services/issues/set_crm_contacts_service_spec.rb b/spec/services/issues/set_crm_contacts_service_spec.rb
index 628f70efad6..2418f317551 100644
--- a/spec/services/issues/set_crm_contacts_service_spec.rb
+++ b/spec/services/issues/set_crm_contacts_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Issues::SetCrmContactsService do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 4739b7e0f28..11ed47b84d9 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Issues::UpdateService, :mailer do
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:guest) { create(:user) }
- let_it_be(:group) { create(:group, :public) }
+ let_it_be(:group) { create(:group, :public, :crm_enabled) }
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
@@ -22,10 +22,10 @@ RSpec.describe Issues::UpdateService, :mailer do
end
before_all do
- project.add_maintainer(user)
- project.add_developer(user2)
- project.add_developer(user3)
- project.add_guest(guest)
+ group.add_maintainer(user)
+ group.add_developer(user2)
+ group.add_developer(user3)
+ group.add_guest(guest)
end
describe 'execute' do
@@ -191,11 +191,6 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
- it 'adds a `incident` label if one does not exist' do
- expect { update_issue(issue_type: 'incident') }.to change(issue.labels, :count).by(1)
- expect(issue.labels.pluck(:title)).to eq(['incident'])
- end
-
it 'creates system note about issue type' do
update_issue(issue_type: 'incident')
@@ -204,6 +199,13 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(note).not_to eq(nil)
end
+ it 'creates an escalation status' do
+ expect { update_issue(issue_type: 'incident') }
+ .to change { issue.reload.incident_management_issuable_escalation_status }
+ .from(nil)
+ .to(a_kind_of(IncidentManagement::IssuableEscalationStatus))
+ end
+
context 'for an issue with multiple labels' do
let(:issue) { create(:incident, project: project, labels: [label_1]) }
@@ -215,18 +217,6 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.labels).to eq([label_1])
end
end
-
- context 'filtering the incident label' do
- let(:params) { { add_label_ids: [] } }
-
- before do
- update_issue(issue_type: 'incident')
- end
-
- it 'creates and add a incident label id to add_label_ids' do
- expect(issue.label_ids).to contain_exactly(label_1.id)
- end
- end
end
context 'from incident to issue' do
@@ -241,10 +231,8 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'for an incident with multiple labels' do
let(:issue) { create(:incident, project: project, labels: [label_1, label_2]) }
- it 'removes an `incident` label if one exists on the incident' do
- expect { update_issue(issue_type: 'issue') }.to change(issue, :label_ids)
- .from(containing_exactly(label_1.id, label_2.id))
- .to([label_2.id])
+ it 'does not remove an `incident` label if one exists on the incident' do
+ expect { update_issue(issue_type: 'issue') }.to not_change(issue, :label_ids)
end
end
@@ -252,10 +240,8 @@ RSpec.describe Issues::UpdateService, :mailer do
let(:issue) { create(:incident, project: project, labels: [label_1, label_2]) }
let(:params) { { label_ids: [label_1.id, label_2.id], remove_label_ids: [] } }
- it 'adds an incident label id to remove_label_ids for it to be removed' do
- expect { update_issue(issue_type: 'issue') }.to change(issue, :label_ids)
- .from(containing_exactly(label_1.id, label_2.id))
- .to([label_2.id])
+ it 'does not add an incident label id to remove_label_ids for it to be removed' do
+ expect { update_issue(issue_type: 'issue') }.to not_change(issue, :label_ids)
end
end
end
@@ -1157,6 +1143,83 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
+ context 'updating escalation status' do
+ let(:opts) { { escalation_status: { status: 'acknowledged' } } }
+ let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
+
+ shared_examples 'updates the escalation status record' do |expected_status|
+ let(:service_double) { instance_double(escalation_update_class) }
+
+ it 'has correct value' do
+ expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double)
+ expect(service_double).to receive(:execute)
+
+ update_issue(opts)
+
+ expect(issue.escalation_status.status_name).to eq(expected_status)
+ end
+ end
+
+ shared_examples 'does not change the status record' do
+ it 'retains the original value' do
+ expected_status = issue.escalation_status&.status_name
+
+ update_issue(opts)
+
+ expect(issue.escalation_status&.status_name).to eq(expected_status)
+ end
+
+ it 'does not trigger side-effects' do
+ expect(escalation_update_class).not_to receive(:new)
+
+ update_issue(opts)
+ end
+ end
+
+ context 'when issue is an incident' do
+ let(:issue) { create(:incident, project: project) }
+
+ context 'with an escalation status record' do
+ before do
+ create(:incident_management_issuable_escalation_status, issue: issue)
+ end
+
+ it_behaves_like 'updates the escalation status record', :acknowledged
+
+ context 'with associated alert' do
+ let!(:alert) { create(:alert_management_alert, issue: issue, project: project) }
+
+ it 'syncs the update back to the alert' do
+ update_issue(opts)
+
+ expect(issue.escalation_status.status_name).to eq(:acknowledged)
+ expect(alert.reload.status_name).to eq(:acknowledged)
+ end
+ end
+
+ context 'with unsupported status value' do
+ let(:opts) { { escalation_status: { status: 'unsupported-status' } } }
+
+ it_behaves_like 'does not change the status record'
+ end
+
+ context 'with status value defined but unchanged' do
+ let(:opts) { { escalation_status: { status: issue.escalation_status.status_name } } }
+
+ it_behaves_like 'does not change the status record'
+ end
+ end
+
+ context 'without an escalation status record' do
+ it_behaves_like 'does not change the status record'
+ end
+ end
+
+ context 'when issue type is not incident' do
+ it_behaves_like 'does not change the status record'
+ end
+ end
+
context 'duplicate issue' do
let(:canonical_issue) { create(:issue, project: project) }
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index 05190accb33..e67ab6025a5 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -109,15 +109,5 @@ RSpec.describe Labels::TransferService do
end
end
- context 'with use_optimized_group_labels_query FF on' do
- it_behaves_like 'transfer labels'
- end
-
- context 'with use_optimized_group_labels_query FF off' do
- before do
- stub_feature_flags(use_optimized_group_labels_query: false)
- end
-
- it_behaves_like 'transfer labels'
- end
+ it_behaves_like 'transfer labels'
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index b9f382d3cd8..1a1283b1078 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe Members::DestroyService do
end
context 'deletion of invitations created by deleted project member' do
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:member_user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 7b9ae19f038..8ceb9979f33 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline do
let_it_be(:project, reload: true) { create(:project) }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:project_user) { create(:user) }
let_it_be(:namespace) { project.namespace }
diff --git a/spec/services/merge_requests/base_service_spec.rb b/spec/services/merge_requests/base_service_spec.rb
index 7911392ef19..6eeba3029ae 100644
--- a/spec/services/merge_requests/base_service_spec.rb
+++ b/spec/services/merge_requests/base_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe MergeRequests::BaseService do
}
end
- subject { MergeRequests::CreateService.new(project: project, current_user: project.owner, params: params) }
+ subject { MergeRequests::CreateService.new(project: project, current_user: project.first_owner, params: params) }
describe '#execute_hooks' do
shared_examples 'enqueues Jira sync worker' do
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index 4b21812503e..e5bea0e7b14 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe MergeRequests::SquashService do
include GitHelpers
let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request }) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
let(:log_error) { "Failed to squash merge request #{merge_request.to_reference(full: true)}:" }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 6ec2b158d30..2925dad7f6b 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -1132,7 +1132,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
context 'updating `force_remove_source_branch`' do
let(:target_project) { create(:project, :repository, :public) }
let(:source_project) { fork_project(target_project, nil, repository: true) }
- let(:user) { target_project.owner }
+ let(:user) { target_project.first_owner }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 793e9ed9848..1fb50b07b3b 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -402,7 +402,7 @@ RSpec.describe Notes::CreateService do
let_it_be(:design) { create(:design, :with_file) }
let_it_be(:project) { design.project }
- let_it_be(:user) { project.owner }
+ let_it_be(:user) { project.first_owner }
let_it_be(:params) do
{
type: 'DiffNote',
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 24775ce06a4..9cbc16f0c95 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2885,7 +2885,7 @@ RSpec.describe NotificationService, :mailer do
let(:member) { create(:user) }
before do
- project.add_developer(member, current_user: project.owner)
+ project.add_developer(member, current_user: project.first_owner)
end
it do
@@ -3287,7 +3287,7 @@ RSpec.describe NotificationService, :mailer do
let_it_be(:domain, reload: true) { create(:pages_domain, project: project) }
let_it_be(:u_blocked) { create(:user, :blocked) }
let_it_be(:u_silence) { create_user_with_notification(:disabled, 'silent', project) }
- let_it_be(:u_owner) { project.owner }
+ let_it_be(:u_owner) { project.first_owner }
let_it_be(:u_maintainer1) { create(:user) }
let_it_be(:u_maintainer2) { create(:user) }
let_it_be(:u_developer) { create(:user) }
@@ -3395,7 +3395,7 @@ RSpec.describe NotificationService, :mailer do
let(:remote_mirror) { create(:remote_mirror, project: project) }
let(:u_blocked) { create(:user, :blocked) }
let(:u_silence) { create_user_with_notification(:disabled, 'silent-maintainer', project) }
- let(:u_owner) { project.owner }
+ let(:u_owner) { project.first_owner }
let(:u_maintainer1) { create(:user) }
let(:u_maintainer2) { create(:user) }
let(:u_developer) { create(:user) }
@@ -3489,7 +3489,7 @@ RSpec.describe NotificationService, :mailer do
it 'sends the email to owners and masters' do
expect(Notify).to receive(:prometheus_alert_fired_email).with(project, master, alert).and_call_original
- expect(Notify).to receive(:prometheus_alert_fired_email).with(project, project.owner, alert).and_call_original
+ expect(Notify).to receive(:prometheus_alert_fired_email).with(project, project.first_owner, alert).and_call_original
expect(Notify).not_to receive(:prometheus_alert_fired_email).with(project, developer, alert)
subject.prometheus_alerts_fired(project, [alert])
diff --git a/spec/services/packages/create_event_service_spec.rb b/spec/services/packages/create_event_service_spec.rb
index 122f1e88ad0..58fa68b11fe 100644
--- a/spec/services/packages/create_event_service_spec.rb
+++ b/spec/services/packages/create_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Packages::CreateEventService do
- let(:scope) { 'container' }
+ let(:scope) { 'generic' }
let(:event_name) { 'push_package' }
let(:params) do
@@ -75,24 +75,24 @@ RSpec.describe Packages::CreateEventService do
context 'with a user' do
let(:user) { create(:user) }
- it_behaves_like 'db package event creation', 'user', 'container'
- it_behaves_like 'redis package unique event creation', 'user', 'container'
- it_behaves_like 'redis package count event creation', 'user', 'container'
+ it_behaves_like 'db package event creation', 'user', 'generic'
+ it_behaves_like 'redis package unique event creation', 'user', 'generic'
+ it_behaves_like 'redis package count event creation', 'user', 'generic'
end
context 'with a deploy token' do
let(:user) { create(:deploy_token) }
- it_behaves_like 'db package event creation', 'deploy_token', 'container'
- it_behaves_like 'redis package unique event creation', 'deploy_token', 'container'
- it_behaves_like 'redis package count event creation', 'deploy_token', 'container'
+ it_behaves_like 'db package event creation', 'deploy_token', 'generic'
+ it_behaves_like 'redis package unique event creation', 'deploy_token', 'generic'
+ it_behaves_like 'redis package count event creation', 'deploy_token', 'generic'
end
context 'with no user' do
let(:user) { nil }
- it_behaves_like 'db package event creation', 'guest', 'container'
- it_behaves_like 'redis package count event creation', 'guest', 'container'
+ it_behaves_like 'db package event creation', 'guest', 'generic'
+ it_behaves_like 'redis package count event creation', 'guest', 'generic'
end
context 'with a package as scope' do
diff --git a/spec/services/packages/maven/metadata/sync_service_spec.rb b/spec/services/packages/maven/metadata/sync_service_spec.rb
index 30ddb48207a..a736ed281f0 100644
--- a/spec/services/packages/maven/metadata/sync_service_spec.rb
+++ b/spec/services/packages/maven/metadata/sync_service_spec.rb
@@ -265,4 +265,22 @@ RSpec.describe ::Packages::Maven::Metadata::SyncService do
end
end
end
+
+ # TODO When cleaning up packages_installable_package_files, consider adding a
+ # dummy package file pending for destruction on L10/11 and remove this context
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: versionless_package_for_versions, file_name: Packages::Maven::Metadata.filename) }
+
+ subject { service.send(:metadata_package_file_for, versionless_package_for_versions) }
+
+ it { is_expected.not_to eq(package_file_pending_destruction) }
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it { is_expected.to eq(package_file_pending_destruction) }
+ end
+ end
end
diff --git a/spec/services/packages/terraform_module/create_package_service_spec.rb b/spec/services/packages/terraform_module/create_package_service_spec.rb
index f911bb5b82c..e172aa726fd 100644
--- a/spec/services/packages/terraform_module/create_package_service_spec.rb
+++ b/spec/services/packages/terraform_module/create_package_service_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Packages::TerraformModule::CreatePackageService do
let!(:existing_package) { create(:terraform_module_package, project: project2, name: 'foo/bar', version: '1.0.0') }
it { expect(subject[:http_status]).to eq 403 }
- it { expect(subject[:message]).to be 'Package already exists.' }
+ it { expect(subject[:message]).to be 'Access Denied' }
end
context 'version already exists' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 2aa9be5066f..d5fbf96ce74 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe Projects::CreateService, '#execute' do
project = create_project(user, opts)
expect(project).to be_valid
- expect(project.owner).to eq(user)
+ expect(project.first_owner).to eq(user)
expect(project.team.maintainers).to include(user)
expect(project.namespace).to eq(user.namespace)
expect(project.project_namespace).to be_in_sync_with_project(project)
@@ -154,6 +154,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_persisted
expect(project.owner).to eq(user)
+ expect(project.first_owner).to eq(user)
expect(project.team.maintainers).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace)
expect(project.project_namespace).to be_in_sync_with_project(project)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index b22f276ee1f..9475f562d71 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
create(:ci_pipeline_artifact, pipeline: pipeline)
create_list(:ci_build_trace_chunk, 3, build: builds[0])
- expect { destroy_project(project, project.owner, {}) }.not_to exceed_query_limit(recorder)
+ expect { destroy_project(project, project.first_owner, {}) }.not_to exceed_query_limit(recorder)
end
it_behaves_like 'deleting the project'
@@ -78,6 +78,11 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end.not_to raise_error
end
+ it 'reports the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
+ destroy_project(project, user, {})
+ end
+
it 'unmarks the project as "pending deletion"' do
destroy_project(project, user, {})
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 3f58fa46806..ce30a20edf4 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Projects::ForkService do
it { expect(to_project).to be_persisted }
it { expect(to_project.errors).to be_empty }
- it { expect(to_project.owner).to eq(@to_user) }
+ it { expect(to_project.first_owner).to eq(@to_user) }
it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
it { expect(to_project.description).to eq(@from_project.description) }
@@ -274,7 +274,7 @@ RSpec.describe Projects::ForkService do
expect(to_project).to be_persisted
expect(to_project.errors).to be_empty
- expect(to_project.owner).to eq(@group)
+ expect(to_project.first_owner).to eq(@group_owner)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 3bd96ad19bc..0d0bb317df2 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -224,6 +224,78 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
end
end
+
+ context 'when payload exceeds max amount of processable alerts' do
+ # We are defining 2 alerts in payload_raw above
+ let(:max_alerts) { 1 }
+
+ before do
+ stub_const("#{described_class}::PROCESS_MAX_ALERTS", max_alerts)
+
+ create(:prometheus_integration, project: project)
+ create(:project_alerting_setting, project: project, token: token)
+
+ allow(Gitlab::AppLogger).to receive(:warn)
+ end
+
+ shared_examples 'process truncated alerts' do
+ it 'returns 200 but skips processing and logs a warning', :aggregate_failures do
+ expect(subject).to be_success
+ expect(subject.payload[:alerts].size).to eq(max_alerts)
+ expect(Gitlab::AppLogger)
+ .to have_received(:warn)
+ .with(
+ message: 'Prometheus payload exceeded maximum amount of alerts. Truncating alerts.',
+ project_id: project.id,
+ alerts: {
+ total: 2,
+ max: max_alerts
+ })
+ end
+ end
+
+ shared_examples 'process all alerts' do
+ it 'returns 200 and process alerts without warnings', :aggregate_failures do
+ expect(subject).to be_success
+ expect(subject.payload[:alerts].size).to eq(2)
+ expect(Gitlab::AppLogger).not_to have_received(:warn)
+ end
+ end
+
+ context 'with feature flag globally enabled' do
+ before do
+ stub_feature_flags(prometheus_notify_max_alerts: true)
+ end
+
+ include_examples 'process truncated alerts'
+ end
+
+ context 'with feature flag enabled on project' do
+ before do
+ stub_feature_flags(prometheus_notify_max_alerts: project)
+ end
+
+ include_examples 'process truncated alerts'
+ end
+
+ context 'with feature flag enabled on unrelated project' do
+ let(:another_project) { create(:project) }
+
+ before do
+ stub_feature_flags(prometheus_notify_max_alerts: another_project)
+ end
+
+ include_examples 'process all alerts'
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(prometheus_notify_max_alerts: false)
+ end
+
+ include_examples 'process all alerts'
+ end
+ end
end
context 'with invalid payload' do
diff --git a/spec/services/projects/repository_languages_service_spec.rb b/spec/services/projects/repository_languages_service_spec.rb
index cb61a7a1a3e..50d5fba6b84 100644
--- a/spec/services/projects/repository_languages_service_spec.rb
+++ b/spec/services/projects/repository_languages_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Projects::RepositoryLanguagesService do
- let(:service) { described_class.new(project, project.owner) }
+ let(:service) { described_class.new(project, project.first_owner) }
context 'when detected_repository_languages flag is set' do
let(:project) { create(:project) }
diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb
deleted file mode 100644
index 58939ef4ada..00000000000
--- a/spec/services/projects/update_pages_configuration_service_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::UpdatePagesConfigurationService do
- let(:service) { described_class.new(project) }
-
- describe "#execute" do
- subject { service.execute }
-
- context 'when pages are deployed' do
- let_it_be(:project) do
- create(:project).tap(&:mark_pages_as_deployed)
- end
-
- let(:file) { Tempfile.new('pages-test') }
-
- before do
- allow(service).to receive(:pages_config_file).and_return(file.path)
- end
-
- after do
- file.close
- file.unlink
- end
-
- context 'when configuration changes' do
- it 'updates the config and reloads the daemon' do
- expect(service).to receive(:update_file).with(file.path, an_instance_of(String))
- .and_call_original
- allow(service).to receive(:update_file).with(File.join(::Settings.pages.path, '.update'),
- an_instance_of(String)).and_call_original
-
- expect(subject).to include(status: :success)
- end
-
- it "doesn't update configuration files if updates on legacy storage are disabled" do
- allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
-
- expect(service).not_to receive(:update_file)
-
- expect(subject).to include(status: :success)
- end
- end
-
- context 'when configuration does not change' do
- before do
- # we set the configuration
- service.execute
- end
-
- it 'does not update anything' do
- expect(service).not_to receive(:update_file)
-
- expect(subject).to include(status: :success)
- end
- end
- end
-
- context 'when pages are not deployed' do
- let_it_be(:project) do
- create(:project).tap(&:mark_pages_as_not_deployed)
- end
-
- it 'returns successfully' do
- expect(subject).to eq(status: :success)
- end
-
- it 'does not update the config' do
- expect(service).not_to receive(:update_file)
-
- subject
- end
- end
- end
-end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index f4a6d1b19e7..547641867bc 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Projects::UpdateRemoteMirrorService do
subject(:execute!) { service.execute(remote_mirror, retries) }
before do
- project.repository.add_branch(project.owner, 'existing-branch', 'master')
+ project.repository.add_branch(project.first_owner, 'existing-branch', 'master')
allow(remote_mirror)
.to receive(:update_repository)
@@ -131,32 +131,82 @@ RSpec.describe Projects::UpdateRemoteMirrorService do
expect_next_instance_of(Lfs::PushService) do |service|
expect(service).to receive(:execute)
end
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
execute!
+
+ expect(remote_mirror.update_status).to eq('finished')
+ expect(remote_mirror.last_error).to be_nil
end
- it 'does nothing to an SSH repository' do
- remote_mirror.update!(url: 'ssh://example.com')
+ context 'when LFS objects fail to push' do
+ before do
+ expect_next_instance_of(Lfs::PushService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :error, message: 'unauthorized' })
+ end
+ end
+
+ context 'when remote_mirror_fail_on_lfs feature flag enabled' do
+ it 'fails update' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(
+ hash_including(message: "Error synching remote mirror")).and_call_original
- expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
+ execute!
- execute!
- end
+ expect(remote_mirror.update_status).to eq('failed')
+ expect(remote_mirror.last_error).to eq("Error synchronizing LFS files:\n\nunauthorized\n\n")
+ end
+ end
- it 'does nothing if LFS is disabled' do
- expect(project).to receive(:lfs_enabled?) { false }
+ context 'when remote_mirror_fail_on_lfs feature flag is disabled' do
+ before do
+ stub_feature_flags(remote_mirror_fail_on_lfs: false)
+ end
- expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
+ it 'does not fail update' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(
+ hash_including(message: "Error synching remote mirror")).and_call_original
- execute!
+ execute!
+
+ expect(remote_mirror.update_status).to eq('finished')
+ expect(remote_mirror.last_error).to be_nil
+ end
+ end
end
- it 'does nothing if non-password auth is specified' do
- remote_mirror.update!(auth_method: 'ssh_public_key')
+ context 'with SSH repository' do
+ let(:ssh_mirror) { create(:remote_mirror, project: project, enabled: true) }
+
+ before do
+ allow(ssh_mirror)
+ .to receive(:update_repository)
+ .and_return(double(divergent_refs: []))
+ end
+
+ it 'does nothing to an SSH repository' do
+ ssh_mirror.update!(url: 'ssh://example.com')
- expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
+ expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
- execute!
+ service.execute(ssh_mirror, retries)
+ end
+
+ it 'does nothing if LFS is disabled' do
+ expect(project).to receive(:lfs_enabled?) { false }
+
+ expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
+
+ service.execute(ssh_mirror, retries)
+ end
+
+ it 'does nothing if non-password auth is specified' do
+ ssh_mirror.update!(auth_method: 'ssh_public_key')
+
+ expect_any_instance_of(Lfs::PushService).not_to receive(:execute)
+
+ service.execute(ssh_mirror, retries)
+ end
end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 4923ef169e8..7b5bf1db030 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe Projects::UpdateService do
describe 'when updating project that has forks' do
let(:project) { create(:project, :internal) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:forked_project) { fork_project(project) }
context 'and unlink forks feature flag is off' do
@@ -379,52 +379,6 @@ RSpec.describe Projects::UpdateService do
end
end
- shared_examples 'updating pages configuration' do
- it 'schedules the `PagesUpdateConfigurationWorker` when pages are deployed' do
- project.mark_pages_as_deployed
-
- expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id)
-
- subject
- end
-
- it "does not schedule a job when pages aren't deployed" do
- project.mark_pages_as_not_deployed
-
- expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async).with(project.id)
-
- subject
- end
- end
-
- context 'when updating #pages_https_only', :https_pages_enabled do
- subject(:call_service) do
- update_project(project, admin, pages_https_only: false)
- end
-
- it 'updates the attribute' do
- expect { call_service }
- .to change { project.pages_https_only? }
- .to(false)
- end
-
- it_behaves_like 'updating pages configuration'
- end
-
- context 'when updating #pages_access_level' do
- subject(:call_service) do
- update_project(project, admin, project_feature_attributes: { pages_access_level: ProjectFeature::ENABLED })
- end
-
- it 'updates the attribute' do
- expect { call_service }
- .to change { project.project_feature.pages_access_level }
- .to(ProjectFeature::ENABLED)
- end
-
- it_behaves_like 'updating pages configuration'
- end
-
context 'when updating #emails_disabled' do
it 'updates the attribute for the project owner' do
expect { update_project(project, user, emails_disabled: true) }
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 756c775be9b..0bea3edf203 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ProtectedBranches::CreateService do
let(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:params) do
{
name: name,
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
index 47a048e7033..4e55c72f312 100644
--- a/spec/services/protected_branches/destroy_service_spec.rb
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProtectedBranches::DestroyService do
let(:protected_branch) { create(:protected_branch) }
let(:project) { protected_branch.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
describe '#execute' do
subject(:service) { described_class.new(project, user) }
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index b5cf1a54aff..3d9b77dcfc0 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProtectedBranches::UpdateService do
let(:protected_branch) { create(:protected_branch) }
let(:project) { protected_branch.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:params) { { name: new_name } }
describe '#execute' do
diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb
index 3d06cc9fb6c..31059d17f10 100644
--- a/spec/services/protected_tags/create_service_spec.rb
+++ b/spec/services/protected_tags/create_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ProtectedTags::CreateService do
let(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:params) do
{
name: name,
diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb
index fbd1452a8d1..658a4f5557e 100644
--- a/spec/services/protected_tags/destroy_service_spec.rb
+++ b/spec/services/protected_tags/destroy_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProtectedTags::DestroyService do
let(:protected_tag) { create(:protected_tag) }
let(:project) { protected_tag.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
describe '#execute' do
subject(:service) { described_class.new(project, user) }
diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb
index 22005bb9b89..8d301dcd825 100644
--- a/spec/services/protected_tags/update_service_spec.rb
+++ b/spec/services/protected_tags/update_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe ProtectedTags::UpdateService do
let(:protected_tag) { create(:protected_tag) }
let(:project) { protected_tag.project }
- let(:user) { project.owner }
+ let(:user) { project.first_owner }
let(:params) { { name: new_name } }
describe '#execute' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 77d263f4b70..e56e54db6f4 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe QuickActions::InterpretService do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:public_project) { create(:project, :public, group: group) }
let_it_be(:repository_project) { create(:project, :repository) }
let_it_be(:project) { public_project }
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index 42520ea26b2..5a88929334b 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -7,10 +7,14 @@ RSpec.describe ResourceAccessTokens::CreateService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
+ let_it_be(:group) { create(:group, :private) }
let_it_be(:params) { {} }
+ before do
+ stub_config_setting(host: 'example.com')
+ end
+
describe '#execute' do
- # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'token creation fails' do
let(:resource) { create(:project)}
@@ -31,7 +35,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token]
- expect(access_token.user.reload.user_type).to eq("#{resource_type}_bot")
+ expect(access_token.user.reload.user_type).to eq("project_bot")
expect(access_token.user.created_by_id).to eq(user.id)
end
@@ -88,6 +92,15 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
+ context 'bot email' do
+ it 'check email domain' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.user.email).to end_with("@noreply.#{Gitlab.config.gitlab.host}")
+ end
+ end
+
context 'access level' do
context 'when user does not specify an access level' do
it 'adds the bot user as a maintainer in the resource' do
@@ -112,10 +125,8 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
context 'when user is external' do
- let(:user) { create(:user, :external) }
-
before do
- project.add_maintainer(user)
+ user.update!(external: true)
end
it 'creates resource bot user with external status' do
@@ -162,7 +173,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token]
project_bot = access_token.user
- expect(project.members.find_by(user_id: project_bot.id).expires_at).to eq(nil)
+ expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq(nil)
end
end
end
@@ -183,7 +194,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
access_token = response.payload[:access_token]
project_bot = access_token.user
- expect(project.members.find_by(user_id: project_bot.id).expires_at).to eq(params[:expires_at])
+ expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq(params[:expires_at])
end
end
end
@@ -234,24 +245,41 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
+ shared_examples 'when user does not have permission to create a resource bot' do
+ it_behaves_like 'token creation fails'
+
+ it 'returns the permission error message' do
+ response = subject
+
+ expect(response.error?).to be true
+ expect(response.errors).to include("User does not have permission to create #{resource_type} access token")
+ end
+ end
+
context 'when resource is a project' do
let_it_be(:resource_type) { 'project' }
let_it_be(:resource) { project }
- context 'when user does not have permission to create a resource bot' do
- it_behaves_like 'token creation fails'
-
- it 'returns the permission error message' do
- response = subject
+ it_behaves_like 'when user does not have permission to create a resource bot'
- expect(response.error?).to be true
- expect(response.errors).to include("User does not have permission to create #{resource_type} access token")
+ context 'user with valid permission' do
+ before_all do
+ resource.add_maintainer(user)
end
+
+ it_behaves_like 'allows creation of bot with valid params'
end
+ end
+
+ context 'when resource is a project' do
+ let_it_be(:resource_type) { 'group' }
+ let_it_be(:resource) { group }
+
+ it_behaves_like 'when user does not have permission to create a resource bot'
context 'user with valid permission' do
before_all do
- resource.add_maintainer(user)
+ resource.add_owner(user)
end
it_behaves_like 'allows creation of bot with valid params'
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index 4f4e2ab0c99..3d724a79fef 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -6,11 +6,12 @@ RSpec.describe ResourceAccessTokens::RevokeService do
subject { described_class.new(user, resource, access_token).execute }
let_it_be(:user) { create(:user) }
+ let_it_be(:user_non_priviledged) { create(:user) }
+ let_it_be(:resource_bot) { create(:user, :project_bot) }
let(:access_token) { create(:personal_access_token, user: resource_bot) }
describe '#execute', :sidekiq_inline do
- # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'revokes access token' do
it { expect(subject.success?).to be true }
@@ -79,71 +80,80 @@ RSpec.describe ResourceAccessTokens::RevokeService do
end
end
- context 'when resource is a project' do
- let_it_be(:resource) { create(:project, :private) }
+ shared_examples 'revoke fails' do |resource_type|
+ let_it_be(:other_user) { create(:user) }
- let(:resource_bot) { create(:user, :project_bot) }
+ context "when access token does not belong to this #{resource_type}" do
+ it 'does not find the bot' do
+ other_access_token = create(:personal_access_token, user: other_user)
- before do
- resource.add_maintainer(user)
- resource.add_maintainer(resource_bot)
- end
+ response = described_class.new(user, resource, other_access_token).execute
- it_behaves_like 'revokes access token'
+ expect(response.success?).to be false
+ expect(response.message).to eq("Failed to find bot user")
+ expect(access_token.reload.revoked?).to be false
+ end
+ end
- context 'revoke fails' do
- let_it_be(:other_user) { create(:user) }
+ context 'when user does not have permission to destroy bot' do
+ context "when non-#{resource_type} member tries to delete project bot" do
+ it 'does not allow other user to delete bot' do
+ response = described_class.new(other_user, resource, access_token).execute
- context 'when access token does not belong to this project' do
- it 'does not find the bot' do
- other_access_token = create(:personal_access_token, user: other_user)
+ expect(response.success?).to be false
+ expect(response.message).to eq("#{other_user.name} cannot delete #{access_token.user.name}")
+ expect(access_token.reload.revoked?).to be false
+ end
+ end
- response = described_class.new(user, resource, other_access_token).execute
+ context "when non-priviledged #{resource_type} member tries to delete project bot" do
+ it 'does not allow developer to delete bot' do
+ response = described_class.new(user_non_priviledged, resource, access_token).execute
expect(response.success?).to be false
- expect(response.message).to eq("Failed to find bot user")
+ expect(response.message).to eq("#{user_non_priviledged.name} cannot delete #{access_token.user.name}")
expect(access_token.reload.revoked?).to be false
end
end
+ end
- context 'when user does not have permission to destroy bot' do
- context 'when non-project member tries to delete project bot' do
- it 'does not allow other user to delete bot' do
- response = described_class.new(other_user, resource, access_token).execute
-
- expect(response.success?).to be false
- expect(response.message).to eq("#{other_user.name} cannot delete #{access_token.user.name}")
- expect(access_token.reload.revoked?).to be false
- end
+ context 'when deletion of bot user fails' do
+ before do
+ allow_next_instance_of(::ResourceAccessTokens::RevokeService) do |service|
+ allow(service).to receive(:execute).and_return(false)
end
+ end
+
+ it_behaves_like 'rollback revoke steps'
+ end
+ end
- context 'when non-maintainer project member tries to delete project bot' do
- let(:developer) { create(:user) }
+ context 'when resource is a project' do
+ let_it_be(:resource) { create(:project, :private) }
- before do
- resource.add_developer(developer)
- end
+ before do
+ resource.add_maintainer(user)
+ resource.add_developer(user_non_priviledged)
+ resource.add_maintainer(resource_bot)
+ end
- it 'does not allow developer to delete bot' do
- response = described_class.new(developer, resource, access_token).execute
+ it_behaves_like 'revokes access token'
- expect(response.success?).to be false
- expect(response.message).to eq("#{developer.name} cannot delete #{access_token.user.name}")
- expect(access_token.reload.revoked?).to be false
- end
- end
- end
+ it_behaves_like 'revoke fails', 'project'
+ end
- context 'when deletion of bot user fails' do
- before do
- allow_next_instance_of(::ResourceAccessTokens::RevokeService) do |service|
- allow(service).to receive(:execute).and_return(false)
- end
- end
+ context 'when resource is a group' do
+ let_it_be(:resource) { create(:group, :private) }
- it_behaves_like 'rollback revoke steps'
- end
+ before do
+ resource.add_owner(user)
+ resource.add_maintainer(user_non_priviledged)
+ resource.add_maintainer(resource_bot)
end
+
+ it_behaves_like 'revokes access token'
+
+ it_behaves_like 'revoke fails', 'group'
end
end
end
diff --git a/spec/services/service_ping/submit_service_ping_service_spec.rb b/spec/services/service_ping/submit_service_ping_service_spec.rb
index ca387690e83..2971c9a9309 100644
--- a/spec/services/service_ping/submit_service_ping_service_spec.rb
+++ b/spec/services/service_ping/submit_service_ping_service_spec.rb
@@ -110,6 +110,7 @@ RSpec.describe ServicePing::SubmitService do
context 'when product_intelligence_enabled is true' do
before do
stub_usage_data_connections
+ stub_database_flavor_check
allow(ServicePing::ServicePingSettings).to receive(:product_intelligence_enabled?).and_return(true)
end
@@ -126,6 +127,7 @@ RSpec.describe ServicePing::SubmitService do
context 'when usage ping is enabled' do
before do
stub_usage_data_connections
+ stub_database_flavor_check
stub_application_setting(usage_ping_enabled: true)
end
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index a13ae471b4b..48c8c24212a 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe TestHooks::SystemService do
let_it_be(:project) { create(:project, :repository) }
let(:hook) { create(:system_hook) }
- let(:service) { described_class.new(hook, project.owner, trigger) }
+ let(:service) { described_class.new(hook, project.first_owner, trigger) }
let(:success_result) { { status: :success, http_status: 200, message: 'ok' } }
before do
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index 74340bac055..ab9da82e91c 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Users::CreateService do
context 'when required parameters are provided' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: email, password: 'mydummypass' }
+ { name: 'John Doe', username: 'jduser', email: email, password: Gitlab::Password.test_default }
end
it 'returns a persisted user' do
@@ -82,13 +82,13 @@ RSpec.describe Users::CreateService do
context 'when force_random_password parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: Gitlab::Password.test_default, force_random_password: true }
end
it 'generates random password' do
user = service.execute
- expect(user.password).not_to eq 'mydummypass'
+ expect(user.password).not_to eq Gitlab::Password.test_default
expect(user.password).to be_present
end
end
@@ -99,7 +99,7 @@ RSpec.describe Users::CreateService do
name: 'John Doe',
username: 'jduser',
email: 'jd@example.com',
- password: 'mydummypass',
+ password: Gitlab::Password.test_default,
password_automatically_set: true
}
end
@@ -121,7 +121,7 @@ RSpec.describe Users::CreateService do
context 'when skip_confirmation parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: Gitlab::Password.test_default, skip_confirmation: true }
end
it 'confirms the user' do
@@ -131,7 +131,7 @@ RSpec.describe Users::CreateService do
context 'when reset_password parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: Gitlab::Password.test_default, reset_password: true }
end
it 'resets password even if a password parameter is given' do
@@ -152,7 +152,7 @@ RSpec.describe Users::CreateService do
context 'with nil user' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: Gitlab::Password.test_default, skip_confirmation: true }
end
let(:service) { described_class.new(nil, params) }
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index aa4df93a241..a31902c7f16 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
# triggered twice.
let!(:project) { create(:project) }
- let(:user) { project.namespace.owner }
+ let(:user) { project.namespace.first_owner }
let(:service) { described_class.new(user) }
describe '#execute', :clean_gitlab_redis_shared_state do
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index 952d482f1bd..ac7e619612f 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::UpsertCreditCardValidationService do
- let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
let(:user_id) { user.id }
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
@@ -21,7 +21,7 @@ RSpec.describe Users::UpsertCreditCardValidationService do
end
describe '#execute' do
- subject(:service) { described_class.new(params) }
+ subject(:service) { described_class.new(params, user) }
context 'successfully set credit card validation record for the user' do
context 'when user does not have credit card validation record' do
@@ -42,6 +42,10 @@ RSpec.describe Users::UpsertCreditCardValidationService do
expiration_date: Date.new(expiration_year, 1, 31)
)
end
+
+ it 'sets the requires_credit_card_verification attribute on the user to false' do
+ expect { service.execute }.to change { user.reload.requires_credit_card_verification }.to(false)
+ end
end
context 'when user has credit card validation record' do
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index 2a3b3814065..42f7ebc85f9 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -269,56 +269,6 @@ RSpec.describe VerifyPagesDomainService do
end
end
- context 'pages configuration updates' do
- context 'enabling a disabled domain' do
- let(:domain) { create(:pages_domain, :disabled) }
-
- it 'schedules an update' do
- stub_resolver(domain.domain => domain.verification_code)
-
- expect(domain).to receive(:update_daemon)
-
- service.execute
- end
- end
-
- context 'verifying an enabled domain' do
- let(:domain) { create(:pages_domain) }
-
- it 'schedules an update' do
- stub_resolver(domain.domain => domain.verification_code)
-
- expect(domain).not_to receive(:update_daemon)
-
- service.execute
- end
- end
-
- context 'disabling an expired domain' do
- let(:domain) { create(:pages_domain, :expired) }
-
- it 'schedules an update' do
- stub_resolver
-
- expect(domain).to receive(:update_daemon)
-
- service.execute
- end
- end
-
- context 'failing to verify a disabled domain' do
- let(:domain) { create(:pages_domain, :disabled) }
-
- it 'does not schedule an update' do
- stub_resolver
-
- expect(domain).not_to receive(:update_daemon)
-
- service.execute
- end
- end
- end
-
context 'no verification code' do
let(:domain) { create(:pages_domain) }
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 2aebd2adab9..7d933ea9c5c 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -2,20 +2,12 @@
require 'spec_helper'
-RSpec.describe WebHookService do
+RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state do
include StubRequests
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:project_hook) { create(:project_hook, project: project) }
- let(:headers) do
- {
- 'Content-Type' => 'application/json',
- 'User-Agent' => "GitLab/#{Gitlab::VERSION}",
- 'X-Gitlab-Event' => 'Push Hook'
- }
- end
-
let(:data) do
{ before: 'oldrev', after: 'newrev', ref: 'ref' }
end
@@ -61,6 +53,21 @@ RSpec.describe WebHookService do
end
describe '#execute' do
+ let!(:uuid) { SecureRandom.uuid }
+ let(:headers) do
+ {
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => "GitLab/#{Gitlab::VERSION}",
+ 'X-Gitlab-Event' => 'Push Hook',
+ 'X-Gitlab-Event-UUID' => uuid
+ }
+ end
+
+ before do
+ # Set a stable value for the `X-Gitlab-Event-UUID` header.
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
+ end
+
context 'when token is defined' do
let_it_be(:project_hook) { create(:project_hook, :token) }
@@ -127,11 +134,74 @@ RSpec.describe WebHookService do
expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
end
+ it 'executes and registers the hook with the recursion detection', :aggregate_failures do
+ stub_full_request(project_hook.url, method: :post)
+ cache_key = Gitlab::WebHooks::RecursionDetection.send(:cache_key_for_hook, project_hook)
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ expect { service_instance.execute }.to change {
+ redis.sismember(cache_key, project_hook.id)
+ }.to(true)
+ end
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with(headers: headers)
+ .once
+ end
+
+ it 'executes and logs if a recursive web hook is detected', :aggregate_failures do
+ stub_full_request(project_hook.url, method: :post)
+ Gitlab::WebHooks::RecursionDetection.register!(project_hook)
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ hook_type: 'ProjectHook',
+ hook_name: 'push_hooks',
+ recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
+ 'correlation_id' => kind_of(String)
+ )
+ )
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with(headers: headers)
+ .once
+ end
+
+ it 'executes and logs if the recursion count limit would be exceeded', :aggregate_failures do
+ stub_full_request(project_hook.url, method: :post)
+ stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
+ previous_hooks = create_list(:project_hook, 3)
+ previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ hook_type: 'ProjectHook',
+ hook_name: 'push_hooks',
+ recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
+ 'correlation_id' => kind_of(String)
+ )
+ )
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
+ .with(headers: headers)
+ .once
+ end
+
it 'handles exceptions' do
exceptions = Gitlab::HTTP::HTTP_ERRORS + [
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError
]
+ allow(Gitlab::WebHooks::RecursionDetection).to receive(:block?).and_return(false)
+
exceptions.each do |exception_class|
exception = exception_class.new('Exception message')
project_hook.enable!
@@ -420,6 +490,57 @@ RSpec.describe WebHookService do
end
end
+ context 'recursion detection' do
+ before do
+ # Set a request UUID so `RecursionDetection.block?` will query redis.
+ Gitlab::WebHooks::RecursionDetection.set_request_uuid(SecureRandom.uuid)
+ end
+
+ it 'queues a worker and logs an error if the call chain limit would be exceeded' do
+ stub_const("#{Gitlab::WebHooks::RecursionDetection.name}::COUNT_LIMIT", 3)
+ previous_hooks = create_list(:project_hook, 3)
+ previous_hooks.each { Gitlab::WebHooks::RecursionDetection.register!(_1) }
+
+ expect(WebHookWorker).to receive(:perform_async)
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ hook_type: 'ProjectHook',
+ hook_name: 'push_hooks',
+ recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
+ 'correlation_id' => kind_of(String),
+ 'meta.project' => project.full_path,
+ 'meta.related_class' => 'ProjectHook',
+ 'meta.root_namespace' => project.root_namespace.full_path
+ )
+ )
+
+ service_instance.async_execute
+ end
+
+ it 'queues a worker and logs an error if a recursive call chain is detected' do
+ Gitlab::WebHooks::RecursionDetection.register!(project_hook)
+
+ expect(WebHookWorker).to receive(:perform_async)
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook recursion detected and will be blocked in future',
+ hook_id: project_hook.id,
+ hook_type: 'ProjectHook',
+ hook_name: 'push_hooks',
+ recursion_detection: Gitlab::WebHooks::RecursionDetection.to_log(project_hook),
+ 'correlation_id' => kind_of(String),
+ 'meta.project' => project.full_path,
+ 'meta.related_class' => 'ProjectHook',
+ 'meta.root_namespace' => project.root_namespace.full_path
+ )
+ )
+
+ service_instance.async_execute
+ end
+ end
+
context 'when hook has custom context attributes' do
it 'includes the attributes in the worker context' do
expect(WebHookWorker).to receive(:perform_async) do
diff --git a/spec/services/work_items/build_service_spec.rb b/spec/services/work_items/build_service_spec.rb
new file mode 100644
index 00000000000..6b2e2d8819e
--- /dev/null
+++ b/spec/services/work_items/build_service_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::BuildService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:user) { guest }
+
+ before_all do
+ project.add_guest(guest)
+ end
+
+ describe '#execute' do
+ subject { described_class.new(project: project, current_user: user, params: {}).execute }
+
+ it { is_expected.to be_a(::WorkItem) }
+ end
+end
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
new file mode 100644
index 00000000000..2c054ae59a0
--- /dev/null
+++ b/spec/services/work_items/create_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::CreateService do
+ include AfterNextHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:spam_params) { double }
+
+ describe '#execute' do
+ let(:work_item) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
+
+ before do
+ stub_spam_services
+ end
+
+ context 'when params are valid' do
+ before_all do
+ project.add_guest(user)
+ end
+
+ let(:opts) do
+ {
+ title: 'Awesome work_item',
+ description: 'please fix'
+ }
+ end
+
+ it 'created instance is a WorkItem' do
+ expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
+
+ expect(work_item).to be_persisted
+ expect(work_item).to be_a(::WorkItem)
+ expect(work_item.title).to eq('Awesome work_item')
+ expect(work_item.description).to eq('please fix')
+ expect(work_item.work_item_type.base_type).to eq('issue')
+ end
+ end
+
+ context 'checking spam' do
+ let(:params) do
+ {
+ title: 'Spam work_item'
+ }
+ end
+
+ subject do
+ described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
+ end
+
+ it 'executes SpamActionService' do
+ expect_next_instance_of(
+ Spam::SpamActionService,
+ {
+ spammable: kind_of(WorkItem),
+ spam_params: spam_params,
+ user: an_instance_of(User),
+ action: :create
+ }
+ ) do |instance|
+ expect(instance).to receive(:execute)
+ end
+
+ subject.execute
+ end
+ end
+ end
+end
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index a5efc8348a4..da4a0e8da80 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -53,7 +53,6 @@ module SimpleCovEnv
track_files '{app,config/initializers,config/initializers_before_autoloader,db/post_migrate,haml_lint,lib,rubocop,tooling}/**/*.rb'
add_filter '/vendor/ruby/'
- add_filter '/app/controllers/sherlock/' # Profiling tool used only in development
add_filter '/bin/'
add_filter 'db/fixtures/development/' # Matches EE files as well
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c497f8245fe..6d5036365e1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -290,15 +290,9 @@ RSpec.configure do |config|
stub_feature_flags(diffs_virtual_scrolling: false)
- # The following `vue_issues_list`/`vue_issuables_list` stubs can be removed
+ # The following `vue_issues_list` stub can be removed
# once the Vue issues page has feature parity with the current Haml page
stub_feature_flags(vue_issues_list: false)
- stub_feature_flags(vue_issuables_list: false)
-
- # Disable `refactor_blob_viewer` as we refactor
- # the blob viewer. See the follwing epic for more:
- # https://gitlab.com/groups/gitlab-org/-/epics/5531
- stub_feature_flags(refactor_blob_viewer: false)
# Disable `main_branch_over_master` as we migrate
# from `master` to `main` accross our codebase.
@@ -459,10 +453,23 @@ RSpec.configure do |config|
end
end
+ # Ensures that any Javascript script that tries to make the external VersionCheck API call skips it and returns a response
+ config.before(:each, :js) do
+ allow_any_instance_of(VersionCheck).to receive(:response).and_return({ "severity" => "success" })
+ end
+
config.after(:each, :silence_stdout) do
$stdout = STDOUT
end
+ config.around(:each, stubbing_settings_source: true) do |example|
+ original_instance = ::Settings.instance_variable_get(:@instance)
+
+ example.run
+
+ ::Settings.instance_variable_set(:@instance, original_instance)
+ end
+
config.disable_monkey_patching!
end
diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml
index d6e74349069..fe51488c706 100644
--- a/spec/support/database/cross-database-modification-allowlist.yml
+++ b/spec/support/database/cross-database-modification-allowlist.yml
@@ -1,31 +1 @@
-- "./ee/spec/mailers/notify_spec.rb"
-- "./ee/spec/models/group_member_spec.rb"
-- "./ee/spec/replicators/geo/terraform_state_version_replicator_spec.rb"
-- "./ee/spec/services/ci/retry_build_service_spec.rb"
-- "./spec/controllers/abuse_reports_controller_spec.rb"
-- "./spec/controllers/omniauth_callbacks_controller_spec.rb"
-- "./spec/controllers/projects/issues_controller_spec.rb"
-- "./spec/features/issues/issue_detail_spec.rb"
-- "./spec/features/projects/pipelines/pipeline_spec.rb"
-- "./spec/features/signed_commits_spec.rb"
-- "./spec/helpers/issuables_helper_spec.rb"
-- "./spec/lib/gitlab/auth_spec.rb"
-- "./spec/lib/gitlab/ci/pipeline/chain/create_spec.rb"
-- "./spec/lib/gitlab/email/handler/create_issue_handler_spec.rb"
-- "./spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb"
-- "./spec/lib/gitlab/email/handler/create_note_handler_spec.rb"
-- "./spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb"
-- "./spec/models/ci/build_trace_chunk_spec.rb"
-- "./spec/models/ci/job_artifact_spec.rb"
-- "./spec/models/ci/runner_spec.rb"
-- "./spec/models/clusters/applications/runner_spec.rb"
-- "./spec/models/design_management/version_spec.rb"
-- "./spec/models/hooks/system_hook_spec.rb"
-- "./spec/models/members/project_member_spec.rb"
-- "./spec/models/user_spec.rb"
-- "./spec/models/user_status_spec.rb"
-- "./spec/requests/api/commits_spec.rb"
-- "./spec/services/ci/retry_build_service_spec.rb"
-- "./spec/services/projects/overwrite_project_service_spec.rb"
-- "./spec/workers/merge_requests/create_pipeline_worker_spec.rb"
-- "./spec/workers/repository_cleanup_worker_spec.rb"
+[]
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 316d645f99f..fb70f82ef87 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -67,7 +67,7 @@ module DbCleaner
# Migrate each database individually
with_reestablished_active_record_base do
all_connection_classes.each do |connection_class|
- ActiveRecord::Base.establish_connection(connection_class.connection_db_config)
+ ActiveRecord::Base.establish_connection(connection_class.connection_db_config) # rubocop: disable Database/EstablishConnection
ActiveRecord::Tasks::DatabaseTasks.migrate
end
diff --git a/spec/support/flaky_tests.rb b/spec/support/flaky_tests.rb
index 0c211af695d..5ce55c47aab 100644
--- a/spec/support/flaky_tests.rb
+++ b/spec/support/flaky_tests.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
return unless ENV['CI']
-return unless ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "true"
+return if ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "false"
return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests')
require_relative '../../tooling/rspec_flaky/report'
diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml
index f3755e52b2c..52ae36229a6 100644
--- a/spec/support/gitlab_stubs/gitlab_ci.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci.yml
@@ -9,7 +9,7 @@ before_script:
variables:
DB_NAME: postgres
-types:
+stages:
- test
- deploy
- notify
@@ -36,7 +36,7 @@ staging:
KEY1: value1
KEY2: value2
script: "cap deploy stating"
- type: deploy
+ stage: deploy
tags:
- ruby
- mysql
@@ -46,7 +46,7 @@ staging:
production:
variables:
DB_NAME: mysql
- type: deploy
+ stage: deploy
script:
- cap deploy production
- cap notify
@@ -58,7 +58,7 @@ production:
- /^deploy-.*$/
dockerhub:
- type: notify
+ stage: notify
script: "curl http://dockerhub/URL"
tags:
- ruby
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 722d484609c..70b794f7d82 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -59,7 +59,7 @@ module CycleAnalyticsHelpers
def save_value_stream(custom_value_stream_name)
fill_in 'create-value-stream-name', with: custom_value_stream_name
- page.find_button(s_('CreateValueStreamForm|Create Value Stream')).click
+ page.find_button(s_('CreateValueStreamForm|Create value stream')).click
wait_for_requests
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 923051a2e04..905c439f4d9 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -9,8 +9,13 @@
require 'securerandom'
require 'socket'
require 'logger'
+require 'bundler'
module GitalySetup
+ extend self
+
+ REPOS_STORAGE = 'default'
+
LOGGER = begin
default_name = ENV['CI'] ? 'DEBUG' : 'WARN'
level_name = ENV['GITLAB_TESTING_LOG_LEVEL']&.upcase
@@ -52,11 +57,13 @@ module GitalySetup
def env
{
- 'HOME' => expand_path('tmp/tests'),
'GEM_PATH' => Gem.path.join(':'),
- 'BUNDLE_APP_CONFIG' => File.join(gemfile_dir, '.bundle'),
'BUNDLE_INSTALL_FLAGS' => nil,
+ 'BUNDLE_IGNORE_CONFIG' => '1',
+ 'BUNDLE_PATH' => bundle_path,
'BUNDLE_GEMFILE' => gemfile,
+ 'BUNDLE_JOBS' => '4',
+ 'BUNDLE_RETRY' => '3',
'RUBYOPT' => nil,
# Git hooks can't run during tests as the internal API is not running.
@@ -65,17 +72,20 @@ module GitalySetup
}
end
- # rubocop:disable GitlabSecurity/SystemCommandInjection
- def set_bundler_config
- system('bundle config set --local jobs 4', chdir: gemfile_dir)
- system('bundle config set --local retry 3', chdir: gemfile_dir)
+ def bundle_path
+ # Allow the user to override BUNDLE_PATH if they need to
+ return ENV['GITALY_TEST_BUNDLE_PATH'] if ENV['GITALY_TEST_BUNDLE_PATH']
if ENV['CI']
- bundle_path = expand_path('vendor/gitaly-ruby')
- system('bundle', 'config', 'set', '--local', 'path', bundle_path, chdir: gemfile_dir)
+ expand_path('vendor/gitaly-ruby')
+ else
+ explicit_path = Bundler.configured_bundle_path.explicit_path
+
+ return unless explicit_path
+
+ expand_path(explicit_path)
end
end
- # rubocop:enable GitlabSecurity/SystemCommandInjection
def config_path(service)
case service
@@ -88,6 +98,10 @@ module GitalySetup
end
end
+ def repos_path(storage = REPOS_STORAGE)
+ Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ end
+
def service_binary(service)
case service
when :gitaly, :gitaly2
@@ -97,16 +111,20 @@ module GitalySetup
end
end
+ def run_command(cmd, env: {})
+ system(env, *cmd, exception: true, chdir: tmp_tests_gitaly_dir)
+ end
+
def install_gitaly_gems
- system(env, "make #{tmp_tests_gitaly_dir}/.ruby-bundle", chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection
+ run_command(%W[make #{tmp_tests_gitaly_dir}/.ruby-bundle], env: env)
end
def build_gitaly
- system(env.merge({ 'GIT_VERSION' => nil }), 'make all git', chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection
+ run_command(%w[make all git], env: env.merge('GIT_VERSION' => nil))
end
- def start_gitaly
- start(:gitaly)
+ def start_gitaly(toml = nil)
+ start(:gitaly, toml)
end
def start_gitaly2
@@ -117,14 +135,20 @@ module GitalySetup
start(:praefect)
end
- def start(service)
+ def start(service, toml = nil)
+ toml ||= config_path(service)
args = ["#{tmp_tests_gitaly_bin_dir}/#{service_binary(service)}"]
args.push("-config") if service == :praefect
- args.push(config_path(service))
+ args.push(toml)
+
+ # Ensure user configuration does not affect Git
+ # Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58776#note_547613780
+ env = self.env.merge('HOME' => nil, 'XDG_CONFIG_HOME' => nil)
+
pid = spawn(env, *args, [:out, :err] => "log/#{service}-test.log")
begin
- try_connect!(service)
+ try_connect!(service, toml)
rescue StandardError
Process.kill('TERM', pid)
raise
@@ -161,29 +185,37 @@ module GitalySetup
abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: gemfile_dir)
end
- def read_socket_path(service)
+ def connect_proc(toml)
# This code needs to work in an environment where we cannot use bundler,
# so we cannot easily use the toml-rb gem. This ad-hoc parser should be
# good enough.
- config_text = IO.read(config_path(service))
+ config_text = IO.read(toml)
config_text.lines.each do |line|
- match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/)
+ match_data = line.match(/^\s*(socket_path|listen_addr)\s*=\s*"([^"]*)"$/)
- return match_data[1] if match_data
+ next unless match_data
+
+ case match_data[1]
+ when 'socket_path'
+ return -> { UNIXSocket.new(match_data[2]) }
+ when 'listen_addr'
+ addr, port = match_data[2].split(':')
+ return -> { TCPSocket.new(addr, port.to_i) }
+ end
end
- raise "failed to find socket_path in #{config_path(service)}"
+ raise "failed to find socket_path or listen_addr in #{toml}"
end
- def try_connect!(service)
+ def try_connect!(service, toml)
LOGGER.debug "Trying to connect to #{service}: "
timeout = 20
delay = 0.1
- socket = read_socket_path(service)
+ connect = connect_proc(toml)
Integer(timeout / delay).times do
- UNIXSocket.new(socket)
+ connect.call
LOGGER.debug " OK\n"
return
@@ -194,6 +226,128 @@ module GitalySetup
LOGGER.warn " FAILED to connect to #{service}\n"
- raise "could not connect to #{socket}"
+ raise "could not connect to #{service}"
+ end
+
+ def gitaly_socket_path
+ Gitlab::GitalyClient.address(REPOS_STORAGE).delete_prefix('unix:')
+ end
+
+ def gitaly_dir
+ socket_path = gitaly_socket_path
+ socket_path = File.expand_path(gitaly_socket_path) if expand_path_for_socket?
+
+ File.dirname(socket_path)
+ end
+
+ # Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters:
+ # https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure
+ # that changes in the current working directory don't affect GRPC reconnections.
+ def expand_path_for_socket?
+ !!ENV['CI']
+ end
+
+ def setup_gitaly
+ unless ENV['CI']
+ # In CI Gitaly is built in the setup-test-env job and saved in the
+ # artifacts. So when tests are started, there's no need to build Gitaly.
+ build_gitaly
+ end
+
+ Gitlab::SetupHelper::Gitaly.create_configuration(
+ gitaly_dir,
+ { 'default' => repos_path },
+ force: true,
+ options: {
+ prometheus_listen_addr: 'localhost:9236'
+ }
+ )
+ Gitlab::SetupHelper::Gitaly.create_configuration(
+ gitaly_dir,
+ { 'default' => repos_path },
+ force: true,
+ options: {
+ internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
+ gitaly_socket: "gitaly2.socket",
+ config_filename: "gitaly2.config.toml"
+ }
+ )
+ Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
+ end
+
+ def socket_path(service)
+ File.join(tmp_tests_gitaly_dir, "#{service}.socket")
+ end
+
+ def praefect_socket_path
+ "unix:" + socket_path(:praefect)
+ end
+
+ def stop(pid)
+ Process.kill('KILL', pid)
+ rescue Errno::ESRCH
+ # The process can already be gone if the test run was INTerrupted.
+ end
+
+ def spawn_gitaly(toml = nil)
+ check_gitaly_config!
+
+ pids = []
+
+ if toml
+ pids << start_gitaly(toml)
+ else
+ pids << start_gitaly
+ pids << start_gitaly2
+ pids << start_praefect
+ end
+
+ Kernel.at_exit do
+ # In CI, this function is called by scripts/gitaly-test-spawn, triggered
+ # in a before_script. Gitaly needs to remain running until the container
+ # is stopped.
+ next if ENV['CI']
+ # In Workhorse tests (locally or in CI), this function is called by
+ # scripts/gitaly-test-spawn during `make test`. Gitaly needs to remain
+ # running until `make test` cleans it up.
+ next if ENV['GITALY_PID_FILE']
+
+ pids.each { |pid| stop(pid) }
+ end
+ rescue StandardError
+ raise gitaly_failure_message
+ end
+
+ def gitaly_failure_message
+ message = "gitaly spawn failed\n\n"
+
+ message += "- The `gitaly` binary does not exist: #{gitaly_binary}\n" unless File.exist?(gitaly_binary)
+ message += "- The `praefect` binary does not exist: #{praefect_binary}\n" unless File.exist?(praefect_binary)
+ message += "- The `git` binary does not exist: #{git_binary}\n" unless File.exist?(git_binary)
+
+ message += "\nCheck log/gitaly-test.log for errors.\n"
+
+ unless ci?
+ message += "\nIf binaries are missing, try running `make -C tmp/tests/gitaly build git.`\n"
+ message += "\nOtherwise, try running `rm -rf #{tmp_tests_gitaly_dir}`."
+ end
+
+ message
+ end
+
+ def git_binary
+ File.join(tmp_tests_gitaly_dir, "_build", "deps", "git", "install", "bin", "git")
+ end
+
+ def gitaly_binary
+ File.join(tmp_tests_gitaly_dir, "_build", "bin", "gitaly")
+ end
+
+ def praefect_binary
+ File.join(tmp_tests_gitaly_dir, "_build", "bin", "praefect")
+ end
+
+ def git_binary_exists?
+ File.exist?(git_binary)
end
end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index d9157fa7485..4e0e8dd96ee 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -95,7 +95,7 @@ module LoginHelpers
visit new_user_session_path
fill_in "user_login", with: user.email
- fill_in "user_password", with: "12345678"
+ fill_in "user_password", with: Gitlab::Password.test_default
check 'user_remember_me' if remember
click_button "Sign in"
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index ae031f58bd4..c3459f7bc81 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -92,12 +92,7 @@ module StubGitlabCalls
end
def stub_commonmark_sourcepos_disabled
- render_options =
- if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
- Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C
- else
- Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY
- end
+ render_options = Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS
allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
.to receive(:render_options)
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 5e86b08aa45..d49a14f7f5b 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -91,6 +91,12 @@ module StubObjectStorage
**params)
end
+ def stub_ci_secure_file_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.ci_secure_files.object_store,
+ uploader: Ci::SecureFileUploader,
+ **params)
+ end
+
def stub_terraform_state_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store,
uploader: Terraform::StateUploader,
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index d36bc4e3cb4..5c3ca92c4d0 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'parallel'
+require_relative 'gitaly_setup'
module TestEnv
extend self
@@ -93,7 +94,6 @@ module TestEnv
}.freeze
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
- REPOS_STORAGE = 'default'
SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage')
SETUP_METHODS = %i[setup_gitaly setup_gitlab_shell setup_workhorse setup_factory_repo setup_forked_repo].freeze
@@ -128,7 +128,7 @@ module TestEnv
# Can be overriden
def post_init
- start_gitaly(gitaly_dir)
+ start_gitaly
end
# Clean /tmp/tests
@@ -142,12 +142,15 @@ module TestEnv
end
FileUtils.mkdir_p(
- Gitlab::GitalyClient::StorageSettings.allow_disk_access { TestEnv.repos_path }
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { GitalySetup.repos_path }
)
FileUtils.mkdir_p(SECOND_STORAGE_PATH)
FileUtils.mkdir_p(backup_path)
FileUtils.mkdir_p(pages_path)
FileUtils.mkdir_p(artifacts_path)
+ FileUtils.mkdir_p(lfs_path)
+ FileUtils.mkdir_p(terraform_state_path)
+ FileUtils.mkdir_p(packages_path)
end
def setup_gitlab_shell
@@ -156,111 +159,28 @@ module TestEnv
def setup_gitaly
component_timed_setup('Gitaly',
- install_dir: gitaly_dir,
+ install_dir: GitalySetup.gitaly_dir,
version: Gitlab::GitalyClient.expected_server_version,
- task: "gitlab:gitaly:test_install",
- task_args: [gitaly_dir, repos_path, gitaly_url].compact) do
- Gitlab::SetupHelper::Gitaly.create_configuration(
- gitaly_dir,
- { 'default' => repos_path },
- force: true,
- options: {
- prometheus_listen_addr: 'localhost:9236'
- }
- )
- Gitlab::SetupHelper::Gitaly.create_configuration(
- gitaly_dir,
- { 'default' => repos_path },
- force: true,
- options: {
- internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
- gitaly_socket: "gitaly2.socket",
- config_filename: "gitaly2.config.toml"
- }
- )
- Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
- end
- end
-
- def gitaly_socket_path
- Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
- end
-
- def gitaly_dir
- socket_path = gitaly_socket_path
- socket_path = File.expand_path(gitaly_socket_path) if expand_path?
-
- File.dirname(socket_path)
- end
-
- # Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters:
- # https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure
- # that changes in the current working directory don't affect GRPC reconnections.
- def expand_path?
- !!ENV['CI']
+ task: "gitlab:gitaly:clone",
+ fresh_install: ENV.key?('FORCE_GITALY_INSTALL'),
+ task_args: [GitalySetup.gitaly_dir, GitalySetup.repos_path, gitaly_url].compact) do
+ GitalySetup.setup_gitaly
+ end
end
- def start_gitaly(gitaly_dir)
+ def start_gitaly
if ci?
# Gitaly has been spawned outside this process already
return
end
- spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
- Bundler.with_original_env do
- unless system(spawn_script)
- message = 'gitaly spawn failed'
- message += " (try `rm -rf #{gitaly_dir}` ?)" unless ci?
- raise message
- end
- end
-
- gitaly_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly.pid')))
- gitaly2_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly2.pid')))
- praefect_pid = Integer(File.read(TMP_TEST_PATH.join('praefect.pid')))
-
- Kernel.at_exit do
- pids = [gitaly_pid, gitaly2_pid, praefect_pid]
- pids.each { |pid| stop(pid) }
- end
-
- wait('gitaly')
- wait('praefect')
- end
-
- def stop(pid)
- Process.kill('KILL', pid)
- rescue Errno::ESRCH
- # The process can already be gone if the test run was INTerrupted.
+ GitalySetup.spawn_gitaly
end
def gitaly_url
ENV.fetch('GITALY_REPO_URL', nil)
end
- def socket_path(service)
- TMP_TEST_PATH.join('gitaly', "#{service}.socket").to_s
- end
-
- def praefect_socket_path
- "unix:" + socket_path(:praefect)
- end
-
- def wait(service)
- sleep_time = 10
- sleep_interval = 0.1
- socket = socket_path(service)
-
- Integer(sleep_time / sleep_interval).times do
- Socket.unix(socket)
- return
- rescue StandardError
- sleep sleep_interval
- end
-
- raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds"
- end
-
# Feature specs are run through Workhorse
def setup_workhorse
# Always rebuild the config file
@@ -376,8 +296,7 @@ module TestEnv
def rm_storage_dir(storage, dir)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path
- target_repo_refs_path = File.join(repos_path, dir)
+ target_repo_refs_path = File.join(GitalySetup.repos_path(storage), dir)
FileUtils.remove_dir(target_repo_refs_path)
end
rescue Errno::ENOENT
@@ -385,8 +304,7 @@ module TestEnv
def storage_dir_exists?(storage, dir)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path
- File.exist?(File.join(repos_path, dir))
+ File.exist?(File.join(GitalySetup.repos_path(storage), dir))
end
end
@@ -399,7 +317,7 @@ module TestEnv
end
def repos_path
- @repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ @repos_path ||= GitalySetup.repos_path
end
def backup_path
@@ -414,6 +332,18 @@ module TestEnv
Gitlab.config.artifacts.storage_path
end
+ def lfs_path
+ Gitlab.config.lfs.storage_path
+ end
+
+ def terraform_state_path
+ Gitlab.config.terraform_state.storage_path
+ end
+
+ def packages_path
+ Gitlab.config.packages.storage_path
+ end
+
# When no cached assets exist, manually hit the root path to create them
#
# Otherwise they'd be created by the first test, often timing out and
@@ -512,7 +442,7 @@ module TestEnv
end
end
- def component_timed_setup(component, install_dir:, version:, task:, task_args: [])
+ def component_timed_setup(component, install_dir:, version:, task:, fresh_install: true, task_args: [])
start = Time.now
ensure_component_dir_name_is_correct!(component, install_dir)
@@ -522,7 +452,7 @@ module TestEnv
if component_needs_update?(install_dir, version)
# Cleanup the component entirely to ensure we start fresh
- FileUtils.rm_rf(install_dir)
+ FileUtils.rm_rf(install_dir) if fresh_install
if ENV['SKIP_RAILS_ENV_IN_RAKE']
# When we run `scripts/setup-test-env`, we take care of loading the necessary dependencies
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 5865bafd382..776ea37ffdc 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -183,6 +183,10 @@ module UsageDataHelpers
)
end
+ def stub_database_flavor_check(flavor = nil)
+ allow(ApplicationRecord.database).to receive(:flavor).and_return(flavor)
+ end
+
def clear_memoized_values(values)
values.each { |v| described_class.clear_memoization(v) }
end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index f862a9bc1a4..3134e5c32a3 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -44,7 +44,7 @@ module ExportFileHelper
create(:ci_trigger, project: project)
key = create(:deploy_key)
key.projects << project
- create(:service, project: project)
+ create(:integration, project: project)
create(:project_hook, project: project, token: 'token')
create(:protected_branch, project: project)
diff --git a/spec/support/praefect.rb b/spec/support/praefect.rb
index 3218275c2aa..451b47cc83c 100644
--- a/spec/support/praefect.rb
+++ b/spec/support/praefect.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-require_relative 'helpers/test_env'
+require_relative 'helpers/gitaly_setup'
RSpec.configure do |config|
config.before(:each, :praefect) do
allow(Gitlab.config.repositories.storages['default']).to receive(:[]).and_call_original
allow(Gitlab.config.repositories.storages['default']).to receive(:[]).with('gitaly_address')
- .and_return(TestEnv.praefect_socket_path)
+ .and_return(GitalySetup.praefect_socket_path)
end
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 085f1f13c2c..27967850389 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -142,6 +142,7 @@ RSpec.shared_context 'group navbar structure' do
nav_sub_items: [
_('General'),
_('Integrations'),
+ _('Access Tokens'),
_('Projects'),
_('Repository'),
_('CI/CD'),
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index ad6462dc367..0dfd76de79c 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -8,7 +8,14 @@ RSpec.shared_context 'GroupPolicy context' do
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:non_group_member) { create(:user) }
- let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) }
+ let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only, :crm_enabled) }
+
+ let(:public_permissions) do
+ %i[
+ read_group read_counts
+ read_label read_issue_board_list read_milestone read_issue_board
+ ]
+ end
let(:guest_permissions) do
%i[
@@ -18,8 +25,6 @@ RSpec.shared_context 'GroupPolicy context' do
]
end
- let(:read_group_permissions) { %i[read_label read_issue_board_list read_milestone read_issue_board] }
-
let(:reporter_permissions) do
%i[
admin_label
@@ -28,6 +33,8 @@ RSpec.shared_context 'GroupPolicy context' do
read_metrics_dashboard_annotation
read_prometheus
read_package_settings
+ read_crm_contact
+ read_crm_organization
]
end
@@ -48,22 +55,24 @@ RSpec.shared_context 'GroupPolicy context' do
destroy_package
create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster
- admin_group_runners
]
end
let(:owner_permissions) do
- [
- :owner_access,
- :admin_group,
- :admin_namespace,
- :admin_group_member,
- :change_visibility_level,
- :set_note_created_at,
- :create_subgroup,
- :read_statistics,
- :update_default_branch_protection
- ].compact
+ %i[
+ owner_access
+ admin_group
+ admin_namespace
+ admin_group_member
+ change_visibility_level
+ set_note_created_at
+ create_subgroup
+ read_statistics
+ update_default_branch_protection
+ read_group_runners
+ admin_group_runners
+ register_group_runners
+ ]
end
let(:admin_permissions) { %i[read_confidential_issues] }
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 8a90f887381..c39252cef13 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -50,7 +50,7 @@ RSpec.shared_context 'ProjectPolicy context' do
resolve_note update_build update_commit_status update_container_image
update_deployment update_environment update_merge_request
update_metrics_dashboard_annotation update_pipeline update_release destroy_release
- read_resource_group update_resource_group
+ read_resource_group update_resource_group update_escalation_status
]
end
diff --git a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
index 8affe4ac8f5..08d0be8c7ac 100644
--- a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
@@ -3,44 +3,19 @@
# Requires a context containing:
# - user
# - params
-# - request_full_path
-RSpec.shared_examples 'request exceeding rate limit' do
- context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do
- before do
- stub_application_setting(notes_create_limit: 2)
- 2.times { post :create, params: params }
- end
+RSpec.shared_examples 'create notes request exceeding rate limit' do
+ include_examples 'rate limited endpoint', rate_limit_key: :notes_create
- it 'prevents from creating more notes' do
- expect { post :create, params: params }
- .to change { Note.count }.by(0)
+ it 'allows user in allow-list to create notes, even if the case is different', :freeze_time, :clean_gitlab_redis_rate_limiting do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:notes_create).and_return(1)
- expect(response).to have_gitlab_http_status(:too_many_requests)
- expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
- end
+ current_user.update_attribute(:username, current_user.username.titleize)
+ stub_application_setting(notes_create_limit_allowlist: [current_user.username.downcase])
- it 'logs the event in auth.log' do
- attributes = {
- message: 'Application_Rate_Limiter_Request',
- env: :notes_create_request_limit,
- remote_ip: '0.0.0.0',
- request_method: 'POST',
- path: request_full_path,
- user_id: user.id,
- username: user.username
- }
+ request
+ request
- expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
- post :create, params: params
- end
-
- it 'allows user in allow-list to create notes, even if the case is different' do
- user.update_attribute(:username, user.username.titleize)
- stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"])
-
- post :create, params: params
- expect(response).to have_gitlab_http_status(:found)
- end
+ expect(response).to have_gitlab_http_status(:found)
end
end
diff --git a/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb
new file mode 100644
index 00000000000..bb2a4159071
--- /dev/null
+++ b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+#
+# Requires a context containing:
+# - request (use method definition to avoid memoizing!)
+# - current_user
+# - error_message # optional
+
+RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:|
+ context 'when rate limiter enabled', :freeze_time, :clean_gitlab_redis_rate_limiting do
+ let(:expected_logger_attributes) do
+ {
+ message: 'Application_Rate_Limiter_Request',
+ env: :"#{rate_limit_key}_request_limit",
+ remote_ip: kind_of(String),
+ request_method: kind_of(String),
+ path: kind_of(String),
+ user_id: current_user.id,
+ username: current_user.username
+ }
+ end
+
+ let(:error_message) { _('This endpoint has been requested too many times. Try again later.') }
+
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(rate_limit_key).and_return(1)
+ end
+
+ it 'logs request and declines it when endpoint called more than the threshold' do |example|
+ expect(Gitlab::AuthLogger).to receive(:error).with(expected_logger_attributes).once
+
+ request
+ request
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+
+ if example.metadata[:type] == :controller
+ expect(response.body).to eq(error_message)
+ else # it is API spec
+ expect(response.body).to eq({ message: { error: error_message } }.to_json)
+ end
+ end
+ end
+
+ context 'when rate limiter is disabled' do
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(rate_limit_key).and_return(0)
+ end
+
+ it 'does not log request and does not block the request' do
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+
+ request
+
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
new file mode 100644
index 00000000000..ae246a87bb6
--- /dev/null
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource access tokens missing access rights' do
+ it 'does not show access token page' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_content("Page Not Found")
+ end
+end
+
+RSpec.shared_examples 'resource access tokens creation' do |resource_type|
+ def active_resource_access_tokens
+ find('.table.active-tokens')
+ end
+
+ def created_resource_access_token
+ find('#created-personal-access-token').value
+ end
+
+ it 'allows creation of an access token', :aggregate_failures do
+ name = 'My access token'
+
+ visit resource_settings_access_tokens_path
+ fill_in 'Token name', with: name
+
+ # Set date to 1st of next month
+ find_field('Expiration date').click
+ find('.pika-next').click
+ click_on '1'
+
+ # Scopes
+ check 'api'
+ check 'read_api'
+
+ click_on "Create #{resource_type} access token"
+
+ expect(active_resource_access_tokens).to have_text(name)
+ expect(active_resource_access_tokens).to have_text('in')
+ expect(active_resource_access_tokens).to have_text('api')
+ expect(active_resource_access_tokens).to have_text('read_api')
+ expect(active_resource_access_tokens).to have_text('Maintainer')
+ expect(created_resource_access_token).not_to be_empty
+ end
+end
+
+RSpec.shared_examples 'resource access tokens creation disallowed' do |error_message|
+ before do
+ group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it 'does not show access token creation form' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).not_to have_selector('#new_resource_access_token')
+ end
+
+ it 'shows access token creation disabled text' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_text(error_message)
+ end
+
+ context 'group settings link' do
+ context 'when user is not a group owner' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'does not show group settings link' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).not_to have_link('group settings', href: edit_group_path(group))
+ end
+ end
+
+ context 'with nested groups' do
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, parent: parent_group) }
+
+ context 'when user is not a top level group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'does not show group settings link' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).not_to have_link('group settings', href: edit_group_path(group))
+ end
+ end
+ end
+
+ context 'when user is a group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'shows group settings link' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_link('group settings', href: edit_group_path(group))
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'active resource access tokens' do
+ def active_resource_access_tokens
+ find('.table.active-tokens')
+ end
+
+ it 'shows active access tokens' do
+ visit resource_settings_access_tokens_path
+
+ expect(active_resource_access_tokens).to have_text(resource_access_token.name)
+ end
+
+ context 'when User#time_display_relative is false' do
+ before do
+ user.update!(time_display_relative: false)
+ end
+
+ it 'shows absolute times for expires_at' do
+ visit resource_settings_access_tokens_path
+
+ expect(active_resource_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
+ end
+ end
+end
+
+RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text|
+ def no_resource_access_tokens_message
+ find('.settings-message')
+ end
+
+ it 'allows revocation of an active token' do
+ visit resource_settings_access_tokens_path
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+
+ it 'removes expired tokens from active section' do
+ resource_access_token.update!(expires_at: 5.days.ago)
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+
+ context 'when resource access token creation is not allowed' do
+ before do
+ group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it 'allows revocation of an active token' do
+ visit resource_settings_access_tokens_path
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index d14b4638ca5..ded30f32314 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -19,14 +19,12 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
RSpec.shared_examples 'package details link' do |property|
- let(:package) { packages.first }
-
it 'navigates to the correct url' do
page.within(packages_table_selector) do
click_link package.name
end
- expect(page).to have_current_path(project_package_path(package.project, package))
+ expect(page).to have_current_path(package_details_path)
expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name)
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
new file mode 100644
index 00000000000..a9dac7a391f
--- /dev/null
+++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'labels sidebar widget' do
+ context 'editing labels' do
+ let_it_be(:development) { create(:group_label, group: group, name: 'Development') }
+ let_it_be(:stretch) { create(:label, project: project, name: 'Stretch') }
+ let_it_be(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
+
+ let(:labels_widget) { find('[data-testid="sidebar-labels"]') }
+
+ before do
+ page.within(labels_widget) do
+ click_on 'Edit'
+ end
+
+ wait_for_all_requests
+ end
+
+ it 'shows labels list in the dropdown' do
+ expect(labels_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 4)
+ end
+
+ it 'adds a label' do
+ within(labels_widget) do
+ adds_label(stretch)
+
+ page.within('[data-testid="value-wrapper"]') do
+ expect(page).to have_content(stretch.name)
+ end
+ end
+ end
+
+ it 'removes a label' do
+ within(labels_widget) do
+ adds_label(stretch)
+ page.within('[data-testid="value-wrapper"]') do
+ expect(page).to have_content(stretch.name)
+ end
+
+ click_on 'Remove label'
+
+ wait_for_requests
+
+ page.within('[data-testid="value-wrapper"]') do
+ expect(page).not_to have_content(stretch.name)
+ end
+ end
+ end
+
+ it 'adds first label by pressing enter when search' do
+ within(labels_widget) do
+ page.within('[data-testid="value-wrapper"]') do
+ expect(page).not_to have_content(development.name)
+ end
+
+ fill_in 'Search', with: 'Devel'
+ sleep 1
+ expect(page.all(:css, '[data-testid="dropdown-content"] .gl-new-dropdown-item').length).to eq(1)
+
+ find_field('Search').native.send_keys(:enter)
+ click_button 'Close'
+ wait_for_requests
+
+ page.within('[data-testid="value-wrapper"]') do
+ expect(page).to have_content(development.name)
+ end
+ end
+ end
+
+ it 'escapes XSS when viewing issuable labels' do
+ page.within(labels_widget) do
+ expect(page).to have_content '<script>alert("xss");</script>'
+ end
+ end
+
+ it 'shows option to create a label' do
+ page.within(labels_widget) do
+ expect(page).to have_content 'Create'
+ end
+ end
+
+ context 'creating a label', :js do
+ before do
+ page.within(labels_widget) do
+ page.find('[data-testid="create-label-button"]').click
+ end
+ end
+
+ it 'shows dropdown switches to "create label" section' do
+ page.within(labels_widget) do
+ expect(page.find('[data-testid="dropdown-header"]')).to have_content 'Create'
+ end
+ end
+
+ it 'creates new label' do
+ page.within(labels_widget) do
+ fill_in 'Name new label', with: 'wontfix'
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
+ wait_for_requests
+
+ expect(page).to have_content 'wontfix'
+ end
+ end
+
+ it 'shows error message if label title is taken' do
+ page.within(labels_widget) do
+ fill_in 'Name new label', with: development.title
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
+ wait_for_requests
+
+ page.within('.dropdown-input') do
+ expect(page.find('.gl-alert')).to have_content 'Title'
+ end
+ end
+ end
+ end
+ end
+
+ def adds_label(label)
+ click_button label.name
+ click_button 'Close'
+
+ wait_for_requests
+ end
+end
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index 615f568420e..11d216ff4b6 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -50,6 +50,10 @@ RSpec.shared_examples 'issue boards sidebar' do
it_behaves_like 'date sidebar widget'
end
+ context 'editing issue labels', :js do
+ it_behaves_like 'labels sidebar widget'
+ end
+
context 'in notifications subscription' do
it 'displays notifications toggle', :aggregate_failures do
page.within('[data-testid="sidebar-notifications"]') do
diff --git a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb
index a2c34cdd4a1..601a53ed913 100644
--- a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb
+++ b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb
@@ -233,7 +233,7 @@ RSpec.shared_examples 'snippet visibility' do
project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_visibility.to_s), snippets_access_level: feature_visibility)
if user_type == :external
- member = project.project_member(external)
+ member = project.member(external)
if project.private?
project.add_developer(external) unless member
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
index 51d52cbb901..dc590e23ace 100644
--- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -8,7 +8,7 @@
# There must be a method or let called `mutation` defined that executes
# the mutation.
RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []|
- let(:match_errors) { eq(errors) }
+ let(:match_errors) { match_array(errors) }
it do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb
index 34c58f524cd..05fee45427a 100644
--- a/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb
@@ -1,12 +1,34 @@
# frozen_string_literal: true
RSpec.shared_examples 'permission level for issue mutation is correctly verified' do |raises_for_all_errors = false|
- before do
- issue.assignees = []
- issue.author = user
+ let_it_be(:other_user_author) { create(:user) }
+
+ def issue_attributes(issue)
+ issue.attributes.except(
+ # Description and title can be updated by authors and assignees of the issues
+ 'description',
+ 'title',
+ # Those fields are calculated or expected to be modified during the mutations
+ 'author_id',
+ 'updated_at',
+ 'updated_by_id',
+ 'last_edited_at',
+ 'last_edited_by_id',
+ 'lock_version',
+ # There were spec failures due to nano-second comparisons
+ # this property isn't changed by any mutation so we don't have to verify it
+ 'created_at'
+ )
end
- shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned|
+ let(:expected) { issue_attributes(issue) }
+
+ shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned_and_author|
+ before do
+ issue.assignees = []
+ issue.update!(author: other_user_author)
+ end
+
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
@@ -17,21 +39,25 @@ RSpec.shared_examples 'permission level for issue mutation is correctly verified
end
it 'does not modify issue' do
- if raises_for_all_errors || raise_for_assigned
+ if raises_for_all_errors || raise_for_assigned_and_author
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
else
- expect(subject[:issue]).to eq issue
+ expect(issue_attributes(subject[:issue])).to eq expected
end
end
end
context 'even if author of the issue' do
before do
- issue.author = user
+ issue.update!(author: user)
end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'does not modify issue' do
+ if raises_for_all_errors || raise_for_assigned_and_author
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ else
+ expect(issue_attributes(subject[:issue])).to eq expected
+ end
end
end
end
diff --git a/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb
index 1ddbad1cea7..b0ac742079a 100644
--- a/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb
@@ -1,13 +1,39 @@
# frozen_string_literal: true
RSpec.shared_examples 'permission level for merge request mutation is correctly verified' do
- before do
- merge_request.assignees = []
- merge_request.reviewers = []
- merge_request.author = nil
+ let(:other_user_author) { create(:user) }
+
+ def mr_attributes(mr)
+ mr.attributes.except(
+ # Authors and assignees can edit title, description, target branch and draft status
+ 'title',
+ 'description',
+ 'target_branch',
+ 'draft',
+ # Those fields are calculated or expected to be modified during the mutations
+ 'author_id',
+ 'latest_merge_request_diff_id',
+ 'last_edited_at',
+ 'last_edited_by_id',
+ 'lock_version',
+ 'updated_at',
+ 'updated_by_id',
+ 'merge_status',
+ # There were spec failures due to nano-second comparisons
+ # this property isn't changed by any mutation so we don't have to verify it
+ 'created_at'
+ )
end
- shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned|
+ let(:expected) { mr_attributes(merge_request) }
+
+ shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned_and_author|
+ before do
+ merge_request.assignees = []
+ merge_request.reviewers = []
+ merge_request.update!(author: other_user_author)
+ end
+
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
@@ -18,12 +44,12 @@ RSpec.shared_examples 'permission level for merge request mutation is correctly
end
it 'does not modify merge request' do
- if raise_for_assigned
+ if raise_for_assigned_and_author
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
else
# In some cases we simply do nothing instead of raising
# https://gitlab.com/gitlab-org/gitlab/-/issues/196241
- expect(subject[:merge_request]).to eq merge_request
+ expect(mr_attributes(subject[:merge_request])).to eq expected
end
end
end
@@ -40,11 +66,17 @@ RSpec.shared_examples 'permission level for merge request mutation is correctly
context 'even if author of the merge request' do
before do
- merge_request.author = user
+ merge_request.update!(author: user)
end
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ if raise_for_assigned_and_author
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ else
+ # In some cases we simply do nothing instead of raising
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/196241
+ expect(mr_attributes(subject[:merge_request])).to eq expected
+ end
end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
index 7888ade56eb..213f084be17 100644
--- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
@@ -22,19 +22,19 @@ RSpec.shared_examples 'marks background migration job records' do
end
end
-RSpec.shared_examples 'finalized background migration' do
+RSpec.shared_examples 'finalized background migration' do |worker_class|
it 'processed the scheduled sidekiq queue' do
queued = Sidekiq::ScheduledSet
.new
.select do |scheduled|
- scheduled.klass == 'BackgroundMigrationWorker' &&
+ scheduled.klass == worker_class.name &&
scheduled.args.first == job_class_name
end
expect(queued.size).to eq(0)
end
it 'processed the async sidekiq queue' do
- queued = Sidekiq::Queue.new('BackgroundMigrationWorker')
+ queued = Sidekiq::Queue.new(worker_class.name)
.select { |scheduled| scheduled.klass == job_class_name }
expect(queued.size).to eq(0)
end
@@ -42,8 +42,8 @@ RSpec.shared_examples 'finalized background migration' do
include_examples 'removed tracked jobs', 'pending'
end
-RSpec.shared_examples 'finalized tracked background migration' do
- include_examples 'finalized background migration'
+RSpec.shared_examples 'finalized tracked background migration' do |worker_class|
+ include_examples 'finalized background migration', worker_class
include_examples 'removed tracked jobs', 'succeeded'
end
diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
deleted file mode 100644
index 046c70bf779..00000000000
--- a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default|
- context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores => true)
- end
-
- it 'multi store is enabled' do
- expect(subject.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores => false)
- end
-
- it 'multi store is disabled' do
- expect(subject.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context "with feature flag :#{use_primary_store_as_default} is enabled" do
- before do
- stub_feature_flags(use_primary_store_as_default => true)
- end
-
- it 'primary store is enabled' do
- expect(subject.use_primary_store_as_default?).to be true
- end
- end
-
- context "with feature flag :#{use_primary_store_as_default} is disabled" do
- before do
- stub_feature_flags(use_primary_store_as_default => false)
- end
-
- it 'primary store is disabled' do
- expect(subject.use_primary_store_as_default?).to be false
- end
- end
-end
diff --git a/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb
index e42a927b5ba..c735b98aa23 100644
--- a/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb
@@ -7,13 +7,13 @@ RSpec.shared_examples 'user login operation with unique ip limit' do
end
it 'allows user authenticating from the same ip' do
- expect { operation_from_ip('ip') }.not_to raise_error
- expect { operation_from_ip('ip') }.not_to raise_error
+ expect { operation_from_ip('111.221.4.3') }.not_to raise_error
+ expect { operation_from_ip('111.221.4.3') }.not_to raise_error
end
it 'blocks user authenticating from two distinct ips' do
- expect { operation_from_ip('ip') }.not_to raise_error
- expect { operation_from_ip('ip2') }.to raise_error(Gitlab::Auth::TooManyIps)
+ expect { operation_from_ip('111.221.4.3') }.not_to raise_error
+ expect { operation_from_ip('1.2.2.3') }.to raise_error(Gitlab::Auth::TooManyIps)
end
end
end
@@ -25,13 +25,13 @@ RSpec.shared_examples 'user login request with unique ip limit' do |success_stat
end
it 'allows user authenticating from the same ip' do
- expect(request_from_ip('ip')).to have_gitlab_http_status(success_status)
- expect(request_from_ip('ip')).to have_gitlab_http_status(success_status)
+ expect(request_from_ip('111.221.4.3')).to have_gitlab_http_status(success_status)
+ expect(request_from_ip('111.221.4.3')).to have_gitlab_http_status(success_status)
end
it 'blocks user authenticating from two distinct ips' do
- expect(request_from_ip('ip')).to have_gitlab_http_status(success_status)
- expect(request_from_ip('ip2')).to have_gitlab_http_status(:forbidden)
+ expect(request_from_ip('111.221.4.3')).to have_gitlab_http_status(success_status)
+ expect(request_from_ip('1.2.2.3')).to have_gitlab_http_status(:forbidden)
end
end
end
diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
index 8f3a93de509..42eec74e64f 100644
--- a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
+++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
@@ -55,10 +55,16 @@ RSpec.shared_examples 'cleanup by a loose foreign key' do
end
def find_model
- model.class.find_by(id: model.id)
+ query = model.class
+ # handle composite primary keys
+ connection = model.class.connection
+ connection.primary_keys(model.class.table_name).each do |primary_key|
+ query = query.where(primary_key => model.public_send(primary_key))
+ end
+ query.first
end
- it 'deletes the model' do
+ it 'cleans up (delete or nullify) the model' do
parent.delete
expect(find_model).to be_present
diff --git a/spec/support/shared_examples/metrics/sampler_shared_examples.rb b/spec/support/shared_examples/metrics/sampler_shared_examples.rb
index ebf199c3a8d..cec540cd120 100644
--- a/spec/support/shared_examples/metrics/sampler_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/sampler_shared_examples.rb
@@ -2,26 +2,98 @@
RSpec.shared_examples 'metrics sampler' do |env_prefix|
context 'when sampling interval is passed explicitly' do
- subject { described_class.new(42) }
+ subject(:sampler) { described_class.new(interval: 42, logger: double) }
- specify { expect(subject.interval).to eq(42) }
+ specify { expect(sampler.interval).to eq(42) }
end
context 'when sampling interval is passed through the environment' do
- subject { described_class.new }
+ subject(:sampler) { described_class.new(logger: double) }
before do
stub_env("#{env_prefix}_INTERVAL_SECONDS", '42')
end
- specify { expect(subject.interval).to eq(42) }
+ specify { expect(sampler.interval).to eq(42) }
end
context 'when no sampling interval is passed anywhere' do
- subject { described_class.new }
+ subject(:sampler) { described_class.new(logger: double) }
it 'uses the hardcoded default' do
- expect(subject.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS)
+ expect(sampler.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS)
+ end
+ end
+
+ describe '#start' do
+ include WaitHelpers
+
+ subject(:sampler) { described_class.new(interval: 0.1) }
+
+ it 'calls the sample method on the sampler thread' do
+ sampling_threads = []
+ expect(sampler).to receive(:sample).at_least(:once) { sampling_threads << Thread.current }
+
+ sampler.start
+
+ wait_for('sampler has sampled', max_wait_time: 3) { sampling_threads.any? }
+ expect(sampling_threads.first.name).to eq(sampler.thread_name)
+
+ sampler.stop
+ end
+
+ context 'with warmup set to true' do
+ subject(:sampler) { described_class.new(interval: 0.1, warmup: true) }
+
+ it 'calls the sample method first on the caller thread' do
+ sampling_threads = []
+ current_thread = Thread.current
+ # Instead of sampling, we're keeping track of which thread the sampling happened on.
+ # We want the first sample to be on the spec thread, which would mean a blocking sample
+ # before the actual sampler thread starts.
+ expect(sampler).to receive(:sample).at_least(:once) { sampling_threads << Thread.current }
+
+ sampler.start
+
+ wait_for('sampler has sampled', max_wait_time: 3) { sampling_threads.size == 2 }
+
+ expect(sampling_threads.first).to be(current_thread)
+ expect(sampling_threads.last.name).to eq(sampler.thread_name)
+
+ sampler.stop
+ end
+ end
+ end
+
+ describe '#safe_sample' do
+ let(:logger) { Logger.new(File::NULL) }
+
+ subject(:sampler) { described_class.new(logger: logger) }
+
+ it 'calls #sample once' do
+ expect(sampler).to receive(:sample)
+
+ sampler.safe_sample
+ end
+
+ context 'when sampling fails with error' do
+ before do
+ expect(sampler).to receive(:sample).and_raise "something failed"
+ end
+
+ it 'recovers from errors' do
+ expect { sampler.safe_sample }.not_to raise_error
+ end
+
+ context 'with logger' do
+ let(:logger) { double('logger') }
+
+ it 'logs errors' do
+ expect(logger).to receive(:warn).with(an_instance_of(String))
+
+ expect { sampler.safe_sample }.not_to raise_error
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index 60a02d85a1e..38f5c7be393 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -94,7 +94,7 @@ RSpec.shared_examples 'application settings examples' do
'1:2:3:4:5::7:8',
'[1:2:3:4:5::7:8]',
'[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443',
- 'www.example2.com:8080',
+ 'www.example.org:8080',
'example.com:8080'
]
@@ -114,7 +114,7 @@ RSpec.shared_examples 'application settings examples' do
an_object_having_attributes(domain: 'example.com'),
an_object_having_attributes(domain: 'subdomain.example.com'),
an_object_having_attributes(domain: 'www.example.com'),
- an_object_having_attributes(domain: 'www.example2.com', port: 8080),
+ an_object_having_attributes(domain: 'www.example.org', port: 8080),
an_object_having_attributes(domain: 'example.com', port: 8080)
]
diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
index 7b33a95bfa1..8ee76efc896 100644
--- a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
@@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do
it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end
end
+
+ describe '.open' do
+ subject { all_escalatables.open }
+
+ it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) }
+ end
end
describe '.status_value' do
@@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
+ describe '.open_status?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :is_open_status) do
+ :triggered | true
+ :acknowledged | true
+ :resolved | false
+ :ignored | false
+ nil | false
+ end
+
+ with_them do
+ it 'returns true when the status is open status' do
+ expect(described_class.open_status?(status)).to eq(is_open_status)
+ end
+ end
+ end
+
describe '#trigger' do
subject { escalatable.trigger }
@@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
+ describe '#open?' do
+ it 'returns true when the status is open status' do
+ expect(triggered_escalatable.open?).to be true
+ expect(acknowledged_escalatable.open?).to be true
+ expect(resolved_escalatable.open?).to be false
+ expect(ignored_escalatable.open?).to be false
+ end
+ end
+
private
def factory_from_class(klass)
diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index ad15f82be5e..2a976fb7421 100644
--- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
+RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name|
include StubRequests
- let(:chat_service) { described_class.new }
+ let(:chat_integration) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com' }
def execute_with_options(options)
@@ -17,7 +17,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
end
describe 'Validations' do
- context 'when service is active' do
+ context 'when integration is active' do
before do
subject.active = true
end
@@ -26,7 +26,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
it_behaves_like 'issue tracker integration URL attribute', :webhook
end
- context 'when service is inactive' do
+ context 'when integration is inactive' do
before do
subject.active = false
end
@@ -35,9 +35,9 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
end
end
- shared_examples "triggered #{service_name} service" do |event_type: nil, branches_to_be_notified: nil|
+ shared_examples "triggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil|
before do
- chat_service.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
+ chat_integration.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
end
let!(:stubbed_resolved_hostname) do
@@ -45,14 +45,14 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
end
it "notifies about #{event_type} events" do
- chat_service.execute(data)
+ chat_integration.execute(data)
expect(WebMock).to have_requested(:post, stubbed_resolved_hostname)
end
end
- shared_examples "untriggered #{service_name} service" do |event_type: nil, branches_to_be_notified: nil|
+ shared_examples "untriggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil|
before do
- chat_service.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
+ chat_integration.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
end
let!(:stubbed_resolved_hostname) do
@@ -60,7 +60,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
end
it "notifies about #{event_type} events" do
- chat_service.execute(data)
+ chat_integration.execute(data)
expect(WebMock).not_to have_requested(:post, stubbed_resolved_hostname)
end
end
@@ -69,50 +69,50 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
let_it_be(:user) { create(:user) }
- let(:chat_service) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_service_params)) }
- let(:chat_service_params) { {} }
+ let(:chat_integration) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_integration_params)) }
+ let(:chat_integration_params) { {} }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let!(:stubbed_resolved_hostname) do
stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s
end
- subject(:execute_service) { chat_service.execute(data) }
+ subject(:execute_integration) { chat_integration.execute(data) }
- shared_examples 'calls the service API with the event message' do |event_message|
+ shared_examples 'calls the integration API with the event message' do |event_message|
specify do
expect_next_instance_of(::Slack::Messenger) do |messenger|
expect(messenger).to receive(:ping).with(event_message, anything).and_call_original
end
- execute_service
+ execute_integration
expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
end
end
context 'with username for slack configured' do
- let(:chat_service_params) { { username: 'slack_username' } }
+ let(:chat_integration_params) { { username: 'slack_username' } }
it 'uses the username as an option' do
expect(::Slack::Messenger).to execute_with_options(username: 'slack_username')
- execute_service
+ execute_integration
end
end
context 'push events' do
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
- it_behaves_like 'calls the service API with the event message', /pushed to branch/
+ it_behaves_like 'calls the integration API with the event message', /pushed to branch/
context 'with event channel' do
- let(:chat_service_params) { { push_channel: 'random' } }
+ let(:chat_integration_params) { { push_channel: 'random' } }
it 'uses the right channel for push event' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
end
end
@@ -123,7 +123,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:ref) { 'refs/tags/v1.1.0' }
let(:data) { Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) }
- it_behaves_like 'calls the service API with the event message', /pushed new tag/
+ it_behaves_like 'calls the integration API with the event message', /pushed new tag/
end
context 'issue events' do
@@ -131,15 +131,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { issue.to_hook_data(user) }
- it_behaves_like 'calls the service API with the event message', /Issue (.*?) opened by/
+ it_behaves_like 'calls the integration API with the event message', /Issue (.*?) opened by/
context 'whith event channel' do
- let(:chat_service_params) { { issue_channel: 'random' } }
+ let(:chat_integration_params) { { issue_channel: 'random' } }
it 'uses the right channel for issue event' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
context 'for confidential issues' do
@@ -150,16 +150,16 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
it 'falls back to issue channel' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
context 'and confidential_issue_channel is defined' do
- let(:chat_service_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } }
+ let(:chat_integration_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } }
it 'uses the confidential issue channel when it is defined' do
expect(::Slack::Messenger).to execute_with_options(channel: ['confidential'])
- execute_service
+ execute_integration
end
end
end
@@ -171,15 +171,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { merge_request.to_hook_data(user) }
- it_behaves_like 'calls the service API with the event message', /opened merge request/
+ it_behaves_like 'calls the integration API with the event message', /opened merge request/
context 'with event channel' do
- let(:chat_service_params) { { merge_request_channel: 'random' } }
+ let(:chat_integration_params) { { merge_request_channel: 'random' } }
it 'uses the right channel for merge request event' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
end
end
@@ -189,15 +189,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
- it_behaves_like 'calls the service API with the event message', %r{ created (.*?)wikis/(.*?)|wiki page> in}
+ it_behaves_like 'calls the integration API with the event message', %r{ created (.*?)wikis/(.*?)|wiki page> in}
context 'with event channel' do
- let(:chat_service_params) { { wiki_page_channel: 'random' } }
+ let(:chat_integration_params) { { wiki_page_channel: 'random' } }
it 'uses the right channel for wiki event' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
end
end
@@ -207,7 +207,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) }
- it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/
+ it_behaves_like 'calls the integration API with the event message', /Deploy to (.*?) created/
end
context 'note event' do
@@ -215,15 +215,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
- it_behaves_like 'calls the service API with the event message', /commented on issue/
+ it_behaves_like 'calls the integration API with the event message', /commented on issue/
context 'with event channel' do
- let(:chat_service_params) { { note_channel: 'random' } }
+ let(:chat_integration_params) { { note_channel: 'random' } }
it 'uses the right channel' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
context 'for confidential notes' do
@@ -234,16 +234,16 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
it 'falls back to note channel' do
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
- execute_service
+ execute_integration
end
context 'and confidential_note_channel is defined' do
- let(:chat_service_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } }
+ let(:chat_integration_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } }
it 'uses confidential channel' do
expect(::Slack::Messenger).to execute_with_options(channel: ['confidential'])
- execute_service
+ execute_integration
end
end
end
@@ -256,7 +256,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:project) { create(:project, :repository, creator: user) }
before do
- allow(chat_service).to receive_messages(
+ allow(chat_integration).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
@@ -283,23 +283,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
)
end
- it_behaves_like "triggered #{service_name} service", event_type: "push"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push"
end
context 'notification enabled only for default branch' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all"
end
end
@@ -325,23 +325,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
)
end
- it_behaves_like "triggered #{service_name} service", event_type: "push"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push"
end
context 'notification enabled only for default branch' do
- it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all"
end
end
@@ -367,23 +367,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
)
end
- it_behaves_like "triggered #{service_name} service", event_type: "push"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push"
end
context 'notification enabled only for default branch' do
- it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all"
end
end
@@ -405,23 +405,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
)
end
- it_behaves_like "triggered #{service_name} service", event_type: "push"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push"
end
context 'notification enabled only for default branch' do
- it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all"
end
end
end
@@ -431,7 +431,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:project) { create(:project, :repository, creator: user) }
before do
- allow(chat_service).to receive_messages(
+ allow(chat_integration).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
@@ -452,7 +452,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
Gitlab::DataBuilder::Note.build(commit_note, user)
end
- it_behaves_like "triggered #{service_name} service", event_type: "commit comment"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "commit comment"
end
context 'when merge request comment event executed' do
@@ -465,7 +465,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
Gitlab::DataBuilder::Note.build(merge_request_note, user)
end
- it_behaves_like "triggered #{service_name} service", event_type: "merge request comment"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "merge request comment"
end
context 'when issue comment event executed' do
@@ -478,7 +478,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
Gitlab::DataBuilder::Note.build(issue_note, user)
end
- it_behaves_like "triggered #{service_name} service", event_type: "issue comment"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "issue comment"
end
context 'when snippet comment event executed' do
@@ -491,7 +491,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
Gitlab::DataBuilder::Note.build(snippet_note, user)
end
- it_behaves_like "triggered #{service_name} service", event_type: "snippet comment"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "snippet comment"
end
end
@@ -505,7 +505,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
end
before do
- allow(chat_service).to receive_messages(
+ allow(chat_integration).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
@@ -519,15 +519,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'with default to notify_only_broken_pipelines' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline"
end
context 'with setting notify_only_broken_pipelines to false' do
before do
- chat_service.notify_only_broken_pipelines = false
+ chat_integration.notify_only_broken_pipelines = false
end
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline"
end
end
@@ -542,19 +542,19 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'notification enabled only for default branch' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all"
end
end
@@ -572,19 +572,19 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'notification enabled only for default branch' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all"
end
end
@@ -602,19 +602,19 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'notification enabled only for default branch' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all"
end
end
@@ -628,19 +628,78 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'notification enabled only for default branch' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default"
end
context 'notification enabled only for protected branches' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected"
end
context 'notification enabled only for default and protected branches' do
- it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
end
context 'notification enabled for all branches' do
- it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all"
+ end
+ end
+ end
+ end
+
+ describe 'Deployment events' do
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
+
+ let(:deployment) do
+ create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.now) }
+
+ before do
+ allow(chat_integration).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ stub_full_request(webhook_url, method: :post)
+ end
+
+ it_behaves_like "triggered #{integration_name} integration", event_type: "deployment"
+
+ context 'on a protected branch' do
+ before do
+ create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
+ end
+
+ let(:deployment) do
+ create(:deployment, :success, project: project, sha: project.commit.sha, ref: 'a-protected-branch')
+ end
+
+ context 'notification enabled only for default branch' do
+ it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default"
+ end
+
+ context 'notification enabled only for protected branches' do
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected"
+ end
+
+ context 'notification enabled only for default and protected branches' do
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected"
+ end
+
+ context 'notification enabled for all branches' do
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all"
+ end
+
+ context 'when chat_notification_deployment_protected_branch_filter is disabled' do
+ before do
+ stub_feature_flags(chat_notification_deployment_protected_branch_filter: false)
+ end
+
+ context 'notification enabled only for default branch' do
+ it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default"
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb b/spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb
new file mode 100644
index 00000000000..f974b46f881
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'destructible' do |factory:|
+ let_it_be(:item1) { create(factory, created_at: 1.month.ago, updated_at: 1.day.ago) }
+ let_it_be(:item2) { create(factory, created_at: 1.year.ago, updated_at: 1.year.ago) }
+ let_it_be(:item3) { create(factory, :pending_destruction, created_at: 2.years.ago, updated_at: 1.month.ago) }
+ let_it_be(:item4) { create(factory, :pending_destruction, created_at: 3.years.ago, updated_at: 2.weeks.ago) }
+
+ describe '.next_pending_destruction' do
+ it 'returns the oldest item pending destruction based on updated_at' do
+ expect(described_class.next_pending_destruction(order_by: :updated_at)).to eq(item3)
+ end
+
+ it 'returns the oldest item pending destruction based on created_at' do
+ expect(described_class.next_pending_destruction(order_by: :created_at)).to eq(item4)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
index 2d08de297a3..174b8609337 100644
--- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
@@ -29,7 +29,7 @@ RSpec.shared_examples 'ttl_expirable' do
describe '.active' do
# rubocop:disable Rails/SaveBang
let_it_be(:item1) { create(class_symbol) }
- let_it_be(:item2) { create(class_symbol, :expired) }
+ let_it_be(:item2) { create(class_symbol, :pending_destruction) }
let_it_be(:item3) { create(class_symbol, status: :error) }
# rubocop:enable Rails/SaveBang
@@ -38,17 +38,6 @@ RSpec.shared_examples 'ttl_expirable' do
end
end
- describe '.lock_next_by' do
- let_it_be(:item1) { create(class_symbol, created_at: 1.month.ago, updated_at: 1.day.ago) }
- let_it_be(:item2) { create(class_symbol, created_at: 1.year.ago, updated_at: 1.year.ago) }
- let_it_be(:item3) { create(class_symbol, created_at: 2.years.ago, updated_at: 1.month.ago) }
-
- it 'returns the first item sorted by the argument' do
- expect(described_class.lock_next_by(:updated_at)).to contain_exactly(item2)
- expect(described_class.lock_next_by(:created_at)).to contain_exactly(item3)
- end
- end
-
describe '#read', :freeze_time do
let_it_be(:old_read_at) { 1.day.ago }
let_it_be(:item1) { create(class_symbol, read_at: old_read_at) }
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index d5d137922eb..5b4b8c8fcc1 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'inherited access level as a member of entity' do
let(:parent_entity) { create(:group) }
let(:user) { create(:user) }
- let(:member) { entity.is_a?(Group) ? entity.group_member(user) : entity.project_member(user) }
+ let(:member) { entity.member(user) }
context 'with root parent_entity developer member' do
before do
@@ -49,7 +49,7 @@ RSpec.shared_examples 'inherited access level as a member of entity' do
entity.add_maintainer(non_member_user)
- non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
+ non_member = entity.member(non_member_user)
expect { non_member.update!(access_level: Gitlab::Access::GUEST) }
.to change { non_member.reload.access_level }
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index 750d3dd11e3..3f8c3b8960b 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -198,7 +198,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
describe 'relationships' do
it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) }
it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) }
- it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile').through(:packages) }
end
end
else
@@ -229,6 +228,26 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it 'returns only files from public packages with same codename' do
expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files)
end
+
+ context 'with pending destruction package files' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: public_package_with_same_codename) }
+
+ it 'does not return them' do
+ expect(subject.to_a).not_to include(package_file_pending_destruction)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ subject
+
+ expect(subject.to_a).to include(package_file_pending_destruction)
+ end
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 2e01de2ea84..06326ffac97 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -115,16 +115,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- project.update!(pending_delete: true)
- project.destroy!
+ expect(Projects::DestroyService.new(project, project.owner).execute).to eq(true)
end
it 'does not schedule a namespace statistics worker' do
expect(Namespaces::ScheduleAggregationWorker)
.not_to receive(:perform_async)
- project.update!(pending_delete: true)
- project.destroy!
+ expect(Projects::DestroyService.new(project, project.owner).execute).to eq(true)
end
end
end
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 3d52ed30c62..b43b7946e69 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -124,6 +124,18 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { expect(subject[0, 2]).to contain_exactly(group_1, group_2) }
it { expect(subject[2, 2]).to contain_exactly(nested_group_1, nested_group_2) }
end
+
+ context 'with offset and limit' do
+ subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).offset(1).limit(1).self_and_ancestors }
+
+ it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) }
+ end
+
+ context 'with upto' do
+ subject { described_class.where(id: deep_nested_group_1).self_and_ancestors(upto: nested_group_1.id) }
+
+ it { is_expected.to contain_exactly(deep_nested_group_1) }
+ end
end
describe '.self_and_ancestors' do
@@ -168,6 +180,19 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { is_expected.to contain_exactly(group_1.id, group_2.id) }
end
+
+ context 'with offset and limit' do
+ subject do
+ described_class
+ .where(id: [deep_nested_group_1, deep_nested_group_2])
+ .limit(1)
+ .offset(1)
+ .self_and_ancestor_ids
+ .pluck(:id)
+ end
+
+ it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) }
+ end
end
describe '.self_and_ancestor_ids' do
diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
index 017e55309f7..6cd871d354c 100644
--- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
@@ -1,19 +1,19 @@
# frozen_string_literal: true
-RSpec.shared_examples 'project access tokens available #index' do
- let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
- let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
+RSpec.shared_examples 'GET resource access tokens available' do
+ let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) }
+ let_it_be(:inactive_resource_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
- it 'retrieves active project access tokens' do
+ it 'retrieves active resource access tokens' do
subject
- expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
+ expect(assigns(:active_resource_access_tokens)).to contain_exactly(active_resource_access_token)
end
- it 'retrieves inactive project access tokens' do
+ it 'retrieves inactive resource access tokens' do
subject
- expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
+ expect(assigns(:inactive_resource_access_tokens)).to contain_exactly(inactive_resource_access_token)
end
it 'lists all available scopes' do
@@ -24,15 +24,15 @@ RSpec.shared_examples 'project access tokens available #index' do
it 'retrieves newly created personal access token value' do
token_value = 'random-value'
- allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
+ allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{resource.id}").and_return(token_value)
subject
- expect(assigns(:new_project_access_token)).to eq(token_value)
+ expect(assigns(:new_resource_access_token)).to eq(token_value)
end
end
-RSpec.shared_examples 'project access tokens available #create' do
+RSpec.shared_examples 'POST resource access tokens available' do
def created_token
PersonalAccessToken.order(:created_at).last
end
@@ -40,17 +40,17 @@ RSpec.shared_examples 'project access tokens available #create' do
it 'returns success message' do
subject
- expect(controller).to set_flash[:notice].to match('Your new project access token has been created.')
+ expect(flash[:notice]).to match('Your new access token has been created.')
end
- it 'creates project access token' do
+ it 'creates resource access token' do
access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER
subject
expect(created_token.name).to eq(access_token_params[:name])
expect(created_token.scopes).to eq(access_token_params[:scopes])
expect(created_token.expires_at).to eq(access_token_params[:expires_at])
- expect(project.project_member(created_token.user).access_level).to eq(access_level)
+ expect(resource.member(created_token.user).access_level).to eq(access_level)
end
it 'creates project bot user' do
@@ -90,12 +90,12 @@ RSpec.shared_examples 'project access tokens available #create' do
it 'shows a failure alert' do
subject
- expect(controller).to set_flash[:alert].to match("Failed to create new project access token: Failed!")
+ expect(flash[:alert]).to match("Failed to create new access token: Failed!")
end
end
end
-RSpec.shared_examples 'project access tokens available #revoke' do
+RSpec.shared_examples 'PUT resource access tokens available' do
it 'calls delete user worker' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true)
@@ -105,7 +105,7 @@ RSpec.shared_examples 'project access tokens available #revoke' do
it 'removes membership of bot user' do
subject
- expect(project.reload.bots).not_to include(bot_user)
+ expect(resource.reload.bots).not_to include(bot_user)
end
it 'converts issuables of the bot user to ghost user' do
@@ -121,4 +121,18 @@ RSpec.shared_examples 'project access tokens available #revoke' do
expect(User.exists?(bot_user.id)).to be_falsy
end
+
+ context 'when unsuccessful' do
+ before do
+ allow_next_instance_of(ResourceAccessTokens::RevokeService) do |service|
+ allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
+ end
+ end
+
+ it 'shows a failure alert' do
+ subject
+
+ expect(flash[:alert]).to include("Could not revoke access token")
+ end
+ end
end
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index 2fd5e6a5f91..9f96cb2a164 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -40,7 +40,6 @@ RSpec.shared_examples 'Debian packages upload request' do |status, body = nil|
expect(response.body).to match(body)
end
end
- it_behaves_like 'a package tracking event', described_class.name, 'push_package'
else
it "returns #{status}#{and_body}", :aggregate_failures do
subject
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index d576a5874fd..9385706d991 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -38,4 +38,28 @@ RSpec.shared_examples 'a package with files' do
'fileSha256' => first_file.file_sha256
)
end
+
+ context 'with package files pending destruction' do
+ let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
+
+ let(:response_package_file_ids) { package_files_response.map { |pf| pf['id'] } }
+
+ it 'does not return them' do
+ expect(package.reload.package_files).to include(package_file_pending_destruction)
+
+ expect(response_package_file_ids).not_to include(package_file_pending_destruction.to_global_id.to_s)
+ end
+
+ context 'with packages_installable_package_files disabled' do
+ before(:context) do
+ stub_feature_flags(packages_installable_package_files: false)
+ end
+
+ it 'returns them' do
+ expect(package.reload.package_files).to include(package_file_pending_destruction)
+
+ expect(response_package_file_ids).to include(package_file_pending_destruction.to_global_id.to_s)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index db70bc75c63..290bf58fb6b 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -221,6 +221,7 @@ RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_ex
let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) }
let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) }
let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) }
+
let(:search_term) { 'uMmy' }
let(:take) { 26 }
let(:skip) { 0 }
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index 827ae42f970..23aee912d2d 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -64,12 +64,16 @@ RSpec.shared_examples 'processes never-before-seen recovery alert' do
end
RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' do
- it 'creates AlertManagement::Alert' do
+ it 'creates alerts and returns them in the payload', :aggregate_failures do
expect(Gitlab::AppLogger).not_to receive(:warn)
expect { subject }
.to change(AlertManagement::Alert, :count).by(2)
.and change(Note, :count).by(4)
+
+ expect(subject).to be_success
+ expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert))
+ expect(subject.payload[:alerts].size).to eq(2)
end
it_behaves_like 'processes incident issues'
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index f6e25ee6647..87bf134eeb8 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -71,6 +71,7 @@ end
RSpec.shared_examples 'an accessible' do
before do
stub_feature_flags(container_registry_migration_phase1: false)
+ stub_feature_flags(container_registry_cdn_redirect: false)
end
let(:access) do
@@ -163,6 +164,7 @@ RSpec.shared_examples 'a container registry auth service' do
before do
stub_feature_flags(container_registry_migration_phase1: false)
+ stub_feature_flags(container_registry_cdn_redirect: false)
end
describe '#full_access_token' do
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index 0277cce975a..36b0acf5a51 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -17,16 +17,6 @@ RSpec.shared_examples 'incident issue' do
end
end
-RSpec.shared_examples 'has incident label' do
- let(:label_properties) { attributes_for(:label, :incident) }
-
- it 'has exactly one incident label' do
- expect(issue.labels).to be_one do |label|
- label.slice(*label_properties.keys).symbolize_keys == label_properties
- end
- end
-end
-
# This shared_example requires the following variables:
# - issue (required)
#
@@ -45,6 +35,12 @@ RSpec.shared_examples 'not an incident issue' do
expect(issue.work_item_type.base_type).not_to eq('incident')
end
+ it_behaves_like 'does not have incident label'
+end
+
+RSpec.shared_examples 'does not have incident label' do
+ let(:label_properties) { attributes_for(:label, :incident) }
+
it 'has not an incident label' do
expect(issue.labels).not_to include(have_attributes(label_properties))
end
diff --git a/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb
index 535e7291b7e..856810a4de1 100644
--- a/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb
+++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb
@@ -2,6 +2,8 @@
RSpec.shared_examples 'service ping payload with all expected metrics' do
specify do
+ allow(ApplicationRecord.database).to receive(:flavor).and_return(nil)
+
aggregate_failures do
expected_metrics.each do |metric|
is_expected.to have_usage_metric metric['key_path']
diff --git a/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb
index 9f18174cbc7..e05239a9a36 100644
--- a/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb
+++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb
@@ -2,6 +2,8 @@
RSpec.shared_examples 'service ping payload without restricted metrics' do
specify do
+ allow(ApplicationRecord.database).to receive(:flavor).and_return(nil)
+
aggregate_failures do
restricted_metrics.each do |metric|
is_expected.not_to have_usage_metric metric['key_path']
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
index 7d652be8d05..68e37171ea2 100644
--- a/spec/support/shared_examples/work_item_base_types_importer.rb
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -3,8 +3,8 @@
RSpec.shared_examples 'work item base types importer' do
it 'creates all base work item types' do
# Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite)
- WorkItem::Type.delete_all
+ WorkItems::Type.delete_all
- expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count)
+ expect { subject }.to change(WorkItems::Type, :count).from(0).to(WorkItems::Type::BASE_TYPES.count)
end
end
diff --git a/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb b/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb
index c9014ad549c..26444437826 100644
--- a/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb
@@ -13,12 +13,12 @@ RSpec.shared_examples 'dependency_proxy_cleanup_worker' do
end
context 'with work to do' do
- let_it_be(:artifact1) { create(factory_type, :expired, group: group) }
- let_it_be(:artifact2) { create(factory_type, :expired, group: group, updated_at: 6.months.ago, created_at: 2.years.ago) }
- let_it_be_with_reload(:artifact3) { create(factory_type, :expired, group: group, updated_at: 1.year.ago, created_at: 1.year.ago) }
+ let_it_be(:artifact1) { create(factory_type, :pending_destruction, group: group) }
+ let_it_be(:artifact2) { create(factory_type, :pending_destruction, group: group, updated_at: 6.months.ago, created_at: 2.years.ago) }
+ let_it_be_with_reload(:artifact3) { create(factory_type, :pending_destruction, group: group, updated_at: 1.year.ago, created_at: 1.year.ago) }
let_it_be(:artifact4) { create(factory_type, group: group, updated_at: 2.years.ago, created_at: 2.years.ago) }
- it 'deletes the oldest expired artifact based on updated_at', :aggregate_failures do
+ it 'deletes the oldest artifact pending destruction based on updated_at', :aggregate_failures do
expect(worker).to receive(:log_extra_metadata_on_done).with("#{factory_type}_id".to_sym, artifact3.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:group_id, group.id)
@@ -40,10 +40,8 @@ RSpec.shared_examples 'dependency_proxy_cleanup_worker' do
end
describe '#remaining_work_count' do
- let_it_be(:expired_artifacts) do
- (1..3).map do |_|
- create(factory_type, :expired, group: group)
- end
+ before(:context) do
+ create_list(factory_type, 3, :pending_destruction, group: group)
end
subject { worker.remaining_work_count }
diff --git a/spec/support/system_exit_detected.rb b/spec/support/system_exit_detected.rb
new file mode 100644
index 00000000000..86c6af3ba8c
--- /dev/null
+++ b/spec/support/system_exit_detected.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+SystemExitDetected = Class.new(RuntimeError)
+
+RSpec.configure do |config|
+ config.around do |example|
+ example.run
+ rescue SystemExit
+ # In any cases, we cannot raise SystemExit in the tests,
+ # because it'll skip any following tests from running.
+ # Convert it to something that won't skip everything.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/350060
+ raise SystemExitDetected, "SystemExit should be rescued in the tests!"
+ end
+end
diff --git a/spec/support_specs/database/multiple_databases_spec.rb b/spec/support_specs/database/multiple_databases_spec.rb
index a8692e315fe..b4cfa253813 100644
--- a/spec/support_specs/database/multiple_databases_spec.rb
+++ b/spec/support_specs/database/multiple_databases_spec.rb
@@ -7,13 +7,13 @@ RSpec.describe 'Database::MultipleDatabases' do
context 'when doing establish_connection' do
context 'on ActiveRecord::Base' do
it 'raises exception' do
- expect { ActiveRecord::Base.establish_connection(:main) }.to raise_error /Cannot re-establish/
+ expect { ActiveRecord::Base.establish_connection(:main) }.to raise_error /Cannot re-establish/ # rubocop: disable Database/EstablishConnection
end
context 'when using with_reestablished_active_record_base' do
it 'does not raise exception' do
with_reestablished_active_record_base do
- expect { ActiveRecord::Base.establish_connection(:main) }.not_to raise_error
+ expect { ActiveRecord::Base.establish_connection(:main) }.not_to raise_error # rubocop: disable Database/EstablishConnection
end
end
end
@@ -25,13 +25,13 @@ RSpec.describe 'Database::MultipleDatabases' do
end
it 'raises exception' do
- expect { Ci::ApplicationRecord.establish_connection(:ci) }.to raise_error /Cannot re-establish/
+ expect { Ci::ApplicationRecord.establish_connection(:ci) }.to raise_error /Cannot re-establish/ # rubocop: disable Database/EstablishConnection
end
context 'when using with_reestablished_active_record_base' do
it 'does not raise exception' do
with_reestablished_active_record_base do
- expect { Ci::ApplicationRecord.establish_connection(:main) }.not_to raise_error
+ expect { Ci::ApplicationRecord.establish_connection(:main) }.not_to raise_error # rubocop: disable Database/EstablishConnection
end
end
end
@@ -42,7 +42,7 @@ RSpec.describe 'Database::MultipleDatabases' do
context 'when reconnect is true' do
it 'does not raise exception' do
with_reestablished_active_record_base(reconnect: true) do
- expect { ActiveRecord::Base.connection.execute("SELECT 1") }.not_to raise_error # rubocop:disable Database/MultipleDatabases
+ expect { ApplicationRecord.connection.execute("SELECT 1") }.not_to raise_error
end
end
end
@@ -50,7 +50,7 @@ RSpec.describe 'Database::MultipleDatabases' do
context 'when reconnect is false' do
it 'does raise exception' do
with_reestablished_active_record_base(reconnect: false) do
- expect { ActiveRecord::Base.connection.execute("SELECT 1") }.to raise_error(ActiveRecord::ConnectionNotEstablished) # rubocop:disable Database/MultipleDatabases
+ expect { ApplicationRecord.connection.execute("SELECT 1") }.to raise_error(ActiveRecord::ConnectionNotEstablished)
end
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 99deaa8d154..c5e73aa3b45 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -4,6 +4,7 @@ require 'rake_helper'
RSpec.describe 'gitlab:app namespace rake task', :delete do
let(:enable_registry) { true }
+ let(:backup_types) { %w{db repo uploads builds artifacts pages lfs terraform_state registry packages} }
def tars_glob
Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
@@ -14,7 +15,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
def backup_files
- %w(backup_information.yml artifacts.tar.gz builds.tar.gz lfs.tar.gz pages.tar.gz)
+ %w(backup_information.yml artifacts.tar.gz builds.tar.gz lfs.tar.gz terraform_state.tar.gz pages.tar.gz packages.tar.gz)
end
def backup_directories
@@ -47,7 +48,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask|
+ backup_types.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -71,14 +72,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
allow(YAML).to receive(:load_file)
.and_return({ gitlab_version: gitlab_version })
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
- expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
+ backup_types.each do |subtask|
+ expect(Rake::Task["gitlab:backup:#{subtask}:restore"]).to receive(:invoke)
+ end
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
end
@@ -95,7 +91,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
context 'when the restore directory is not empty' do
before do
# We only need a backup of the repositories for this test
- stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
+ stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,terraform_state,registry')
create(:project, :repository)
end
@@ -139,11 +135,10 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:terraform_state:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:packages:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
-
- # We only need a backup of the repositories for this test
- stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
end
it 'restores the data' do
@@ -202,10 +197,8 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
context 'specific backup tasks' do
- let(:task_list) { %w(db repo uploads builds artifacts pages lfs registry) }
-
it 'prints a progress message to stdout' do
- task_list.each do |task|
+ backup_types.each do |task|
expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout_from_any_process
end
end
@@ -219,16 +212,49 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping artifacts ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping pages ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping lfs objects ... ")
+ expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping terraform states ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping container registry images ... ")
- expect(Gitlab::BackupLogger).to receive(:info).with(message: "done").exactly(7).times
+ expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping packages ... ")
+ expect(Gitlab::BackupLogger).to receive(:info).with(message: "done").exactly(9).times
- task_list.each do |task|
+ backup_types.each do |task|
run_rake_task("gitlab:backup:#{task}:create")
end
end
end
end
+ describe 'backup create fails' do
+ using RSpec::Parameterized::TableSyntax
+
+ file_backup_error = Backup::FileBackupError.new('/tmp', '/tmp/backup/uploads')
+ config = ActiveRecord::Base.configurations.find_db_config(Rails.env).configuration_hash
+ db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
+ db_backup_error = Backup::DatabaseBackupError.new(config, db_file_name)
+
+ where(:backup_class, :rake_task, :error) do
+ Backup::Database | 'gitlab:backup:db:create' | db_backup_error
+ Backup::Builds | 'gitlab:backup:builds:create' | file_backup_error
+ Backup::Uploads | 'gitlab:backup:uploads:create' | file_backup_error
+ Backup::Artifacts | 'gitlab:backup:artifacts:create' | file_backup_error
+ Backup::Pages | 'gitlab:backup:pages:create' | file_backup_error
+ Backup::Lfs | 'gitlab:backup:lfs:create' | file_backup_error
+ Backup::Registry | 'gitlab:backup:registry:create' | file_backup_error
+ end
+
+ with_them do
+ before do
+ expect_next_instance_of(backup_class) do |instance|
+ expect(instance).to receive(:dump).and_raise(error)
+ end
+ end
+
+ it "raises an error with message" do
+ expect { run_rake_task(rake_task) }.to output(Regexp.new(error.message)).to_stdout_from_any_process
+ end
+ end
+ end
+
context 'tar creation' do
context 'archive file permissions' do
it 'sets correct permissions on the tar file' do
@@ -255,9 +281,11 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz terraform_state.tar.gz registry.tar.gz packages.tar.gz}
)
+ puts "CONTENT: #{tar_contents}"
+
expect(exit_status).to eq(0)
expect(tar_contents).to match('db')
expect(tar_contents).to match('uploads.tar.gz')
@@ -266,7 +294,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('terraform_state.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
+ expect(tar_contents).to match('packages.tar.gz')
expect(tar_contents).not_to match(%r{^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)/$})
end
@@ -274,7 +304,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,terraform_state,registry,packages}')
)
expect(temp_dirs).to be_empty
@@ -304,7 +334,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
before do
# We only need a backup of the repositories for this test
- stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
+ stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,terraform_state,registry')
stub_storage_settings( second_storage_name => {
'gitaly_address' => Gitlab.config.repositories.storages.default.gitaly_address,
'path' => TestEnv::SECOND_STORAGE_PATH
@@ -378,7 +408,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
context 'concurrency settings' do
before do
# We only need a backup of the repositories for this test
- stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
+ stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,terraform_state,registry')
create(:project, :repository)
end
@@ -407,7 +437,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.with(max_concurrency: 5, max_storage_concurrency: 2)
.and_call_original
end
- expect(::Backup::GitalyBackup).to receive(:new).with(anything, parallel: 5, parallel_storage: 2).and_call_original
+ expect(::Backup::GitalyBackup).to receive(:new).with(anything, max_parallelism: 5, storage_parallelism: 2).and_call_original
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
end
@@ -425,31 +455,34 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
# backup_create task
- describe "Skipping items" do
+ describe "Skipping items in a backup" do
before do
- stub_env('SKIP', 'repositories,uploads')
+ stub_env('SKIP', 'an-unknown-type,repositories,uploads,anotherunknowntype')
create(:project, :repository)
end
- it "does not contain skipped item" do
+ it "does not contain repositories and uploads" do
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz terraform_state.tar.gz registry.tar.gz packages.tar.gz}
)
expect(tar_contents).to match('db/')
- expect(tar_contents).to match('uploads.tar.gz')
+ expect(tar_contents).to match('uploads.tar.gz: Not found in archive')
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('terraform_state.tar.gz')
expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
+ expect(tar_contents).to match('packages.tar.gz')
expect(tar_contents).not_to match('repositories/')
+ expect(tar_contents).to match('repositories: Not found in archive')
end
- it 'does not invoke repositories restore' do
+ it 'does not invoke restore of repositories and uploads' do
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
allow(Rake::Task['gitlab:shell:setup'])
@@ -463,7 +496,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:terraform_state:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:packages:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
end
@@ -488,8 +523,10 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
'builds.tar.gz',
'artifacts.tar.gz',
'lfs.tar.gz',
+ 'terraform_state.tar.gz',
'pages.tar.gz',
'registry.tar.gz',
+ 'packages.tar.gz',
'repositories'
)
end
@@ -501,14 +538,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.to receive(:invoke).and_return(true)
expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
- expect(Rake::Task['gitlab:backup:db:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:repo:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:uploads:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
- expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
+ backup_types.each do |subtask|
+ expect(Rake::Task["gitlab:backup:#{subtask}:restore"]).to receive :invoke
+ end
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task("gitlab:backup:restore") }.to output.to_stdout_from_any_process
end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 830d0dded2e..92c896b1ab0 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -214,7 +214,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
expect(Gitlab::Database::Reindexing).to receive(:enabled?).and_return(false)
expect(Gitlab::Database::Reindexing).not_to receive(:invoke)
- run_rake_task('gitlab:db:reindex')
+ expect { run_rake_task('gitlab:db:reindex') }.to raise_error(SystemExit)
end
end
end
@@ -233,7 +233,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
expect(Gitlab::Database::Reindexing).to receive(:enabled?).and_return(false)
expect(Gitlab::Database::Reindexing).not_to receive(:invoke).with(database_name)
- run_rake_task("gitlab:db:reindex:#{database_name}")
+ expect { run_rake_task("gitlab:db:reindex:#{database_name}") }.to raise_error(SystemExit)
end
end
end
diff --git a/spec/tasks/gitlab/password_rake_spec.rb b/spec/tasks/gitlab/password_rake_spec.rb
index 65bba836024..ec18d713351 100644
--- a/spec/tasks/gitlab/password_rake_spec.rb
+++ b/spec/tasks/gitlab/password_rake_spec.rb
@@ -3,7 +3,7 @@
require 'rake_helper'
RSpec.describe 'gitlab:password rake tasks', :silence_stdout do
- let_it_be(:user_1) { create(:user, username: 'foobar', password: 'initial_password') }
+ let_it_be(:user_1) { create(:user, username: 'foobar', password: Gitlab::Password.test_default) }
def stub_username(username)
allow(Gitlab::TaskHelpers).to receive(:prompt).with('Enter username: ').and_return(username)
@@ -19,14 +19,14 @@ RSpec.describe 'gitlab:password rake tasks', :silence_stdout do
Rake.application.rake_require 'tasks/gitlab/password'
stub_username('foobar')
- stub_password('secretpassword')
+ stub_password(Gitlab::Password.test_default)
end
describe ':reset' do
context 'when all inputs are correct' do
it 'updates the password properly' do
run_rake_task('gitlab:password:reset', user_1.username)
- expect(user_1.reload.valid_password?('secretpassword')).to eq(true)
+ expect(user_1.reload.valid_password?(Gitlab::Password.test_default)).to eq(true)
end
end
@@ -55,7 +55,7 @@ RSpec.describe 'gitlab:password rake tasks', :silence_stdout do
context 'when passwords do not match' do
before do
- stub_password('randompassword', 'differentpassword')
+ stub_password(Gitlab::Password.test_default, "different" + Gitlab::Password.test_default)
end
it 'aborts with an error' do
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
index acaf9b5729b..442b884b313 100644
--- a/spec/tasks/gitlab/usage_data_rake_spec.rb
+++ b/spec/tasks/gitlab/usage_data_rake_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
Rake.application.rake_require 'tasks/gitlab/usage_data'
# stub prometheus external http calls https://gitlab.com/gitlab-org/gitlab/-/issues/245277
stub_prometheus_queries
+ stub_database_flavor_check
end
describe 'dump_sql_in_yaml' do
diff --git a/spec/tooling/danger/datateam_spec.rb b/spec/tooling/danger/datateam_spec.rb
new file mode 100644
index 00000000000..3bcef3ac886
--- /dev/null
+++ b/spec/tooling/danger/datateam_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'rspec-parameterized'
+require 'gitlab-dangerfiles'
+require 'gitlab/dangerfiles/spec_helper'
+require 'pry'
+require_relative '../../../tooling/danger/datateam'
+
+RSpec.describe Tooling::Danger::Datateam do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:datateam) { fake_danger.new(helper: fake_helper) }
+
+ describe 'data team danger' do
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ 'with structure.sql changes and no Data Warehouse::Impact Check label' => {
+ modified_files: %w(db/structure.sql app/models/user.rb),
+ changed_lines: ['+group_id bigint NOT NULL'],
+ mr_labels: [],
+ impacted: true,
+ impacted_files: %w(db/structure.sql)
+ },
+ 'with structure.sql changes and Data Warehouse::Impact Check label' => {
+ modified_files: %w(db/structure.sql),
+ changed_lines: ['+group_id bigint NOT NULL)'],
+ mr_labels: ['Data Warehouse::Impact Check'],
+ impacted: false,
+ impacted_files: %w(db/structure.sql)
+ },
+ 'with user model changes' => {
+ modified_files: %w(app/models/users.rb),
+ changed_lines: ['+has_one :namespace'],
+ mr_labels: [],
+ impacted: false,
+ impacted_files: []
+ },
+ 'with perfomance indicator changes and no Data Warehouse::Impact Check label' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ changed_lines: ['+-gmau'],
+ mr_labels: [],
+ impacted: true,
+ impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ },
+ 'with perfomance indicator changes and Data Warehouse::Impact Check label' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ changed_lines: ['+-gmau'],
+ mr_labels: ['Data Warehouse::Impact Check'],
+ impacted: false,
+ impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ },
+ 'with metric file changes and no performance indicator changes' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ changed_lines: ['-product_stage: growth'],
+ mr_labels: [],
+ impacted: false,
+ impacted_files: []
+ },
+ 'with metric file changes and no performance indicator changes and other label' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml),
+ changed_lines: ['-product_stage: growth'],
+ mr_labels: ['type::tooling'],
+ impacted: false,
+ impacted_files: []
+ },
+ 'with performance indicator changes and other label' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ changed_lines: ['+-gmau'],
+ mr_labels: ['type::tooling'],
+ impacted: true,
+ impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ },
+ 'with performance indicator changes, Data Warehouse::Impact Check and other label' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ changed_lines: ['+-gmau'],
+ mr_labels: ['type::tooling', 'Data Warehouse::Impact Check'],
+ impacted: false,
+ impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ },
+ 'with performance indicator changes and other labels' => {
+ modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
+ changed_lines: ['+-gmau'],
+ mr_labels: ['type::tooling', 'Data Warehouse::Impacted'],
+ impacted: false,
+ impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
+ }
+ }
+ end
+
+ with_them do
+ before do
+ allow(fake_helper).to receive(:modified_files).and_return(modified_files)
+ allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
+ allow(fake_helper).to receive(:mr_labels).and_return(mr_labels)
+ allow(fake_helper).to receive(:markdown_list).with(impacted_files).and_return(impacted_files.map { |item| "* `#{item}`" }.join("\n"))
+ end
+
+ it :aggregate_failures do
+ expect(datateam.impacted?).to be(impacted)
+ expect(datateam.build_message).to match_expected_message
+ end
+ end
+ end
+
+ def match_expected_message
+ return be_nil unless impacted
+
+ start_with(described_class::CHANGED_SCHEMA_MESSAGE).and(include(*impacted_files))
+ end
+end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index f13083bdf0a..52aa90beb2b 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -269,7 +269,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
- expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, ci_config, database, documentation, duplicate_yarn_dependencies, eslint, gitaly, pajamas, pipeline, prettier, product_intelligence, utility_css, vue_shared_documentation')
+ expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, ci_config, database, documentation, duplicate_yarn_dependencies, eslint, gitaly, pajamas, pipeline, prettier, product_intelligence, utility_css, vue_shared_documentation, datateam')
end
end
diff --git a/spec/tooling/docs/deprecation_handling_spec.rb b/spec/tooling/docs/deprecation_handling_spec.rb
new file mode 100644
index 00000000000..e389fe882b2
--- /dev/null
+++ b/spec/tooling/docs/deprecation_handling_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative '../../fast_spec_helper'
+require_relative '../../../tooling/docs/deprecation_handling'
+require_relative '../../support/helpers/next_instance_of'
+
+RSpec.describe Docs::DeprecationHandling do
+ include ::NextInstanceOf
+
+ let(:type) { 'deprecation' }
+
+ subject { described_class.new(type).render }
+
+ before do
+ allow(Rake::FileList).to receive(:new).and_return(
+ ['14-10-c.yml', '14-2-b.yml', '14-2-a.yml']
+ )
+ # Create dummy YAML data based on file name
+ allow(YAML).to receive(:load_file) do |file_name|
+ {
+ 'name' => file_name[/[a-z]*\.yml/],
+ 'announcement_milestone' => file_name[/\d+-\d+/].tr('-', '.')
+ }
+ end
+ end
+
+ it 'sorts entries and milestones' do
+ allow_next_instance_of(ERB) do |template|
+ expect(template).to receive(:result_with_hash) do |arguments|
+ milestones = arguments[:milestones]
+ entries = arguments[:entries]
+
+ expect(milestones).to eq(['14.2', '14.10'])
+ expect(entries.map { |e| e['name'] }).to eq(['a.yml', 'b.yml', 'c.yml'])
+ end
+ end
+
+ subject
+ end
+end
diff --git a/spec/uploaders/ci/secure_file_uploader_spec.rb b/spec/uploaders/ci/secure_file_uploader_spec.rb
new file mode 100644
index 00000000000..3be4f742a24
--- /dev/null
+++ b/spec/uploaders/ci/secure_file_uploader_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::SecureFileUploader do
+ subject { ci_secure_file.file }
+
+ let(:project) { create(:project) }
+ let(:ci_secure_file) { create(:ci_secure_file) }
+ let(:sample_file) { fixture_file('ci_secure_files/upload-keystore.jks') }
+
+ before do
+ stub_ci_secure_file_object_storage
+ end
+
+ describe '#key' do
+ it 'creates a digest with a secret key and the project id' do
+ expect(OpenSSL::HMAC)
+ .to receive(:digest)
+ .with('SHA256', Gitlab::Application.secrets.db_key_base, ci_secure_file.project_id.to_s)
+ .and_return('digest')
+
+ expect(subject.key).to eq('digest')
+ end
+ end
+
+ describe '.checksum' do
+ it 'returns a SHA256 checksum for the unencrypted file' do
+ expect(subject.checksum).to eq(Digest::SHA256.hexdigest(sample_file))
+ end
+ end
+
+ describe 'encryption' do
+ it 'encrypts the stored file' do
+ expect(Base64.encode64(subject.file.read)).not_to eq(Base64.encode64(sample_file))
+ end
+
+ it 'decrypts the file when reading' do
+ expect(Base64.encode64(subject.read)).to eq(Base64.encode64(sample_file))
+ end
+ end
+
+ describe '.direct_upload_enabled?' do
+ it 'returns false' do
+ expect(described_class.direct_upload_enabled?).to eq(false)
+ end
+ end
+
+ describe '.background_upload_enabled?' do
+ it 'returns false' do
+ expect(described_class.background_upload_enabled?).to eq(false)
+ end
+ end
+
+ describe '.default_store' do
+ context 'when object storage is enabled' do
+ it 'returns REMOTE' do
+ expect(described_class.default_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_ci_secure_file_object_storage(enabled: false)
+ end
+
+ it 'returns LOCAL' do
+ expect(described_class.default_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 9fa95613d1c..9db2bd3741a 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -53,11 +53,14 @@ RSpec.describe 'admin/dashboard/index.html.haml' do
expect(rendered).not_to have_content "Users over License"
end
- it 'links to the GitLab Changelog' do
- stub_application_setting(version_check_enabled: true)
-
- render
+ describe 'when show_version_check? is true' do
+ before do
+ allow(view).to receive(:show_version_check?).and_return(true)
+ render
+ end
- expect(rendered).to have_link(href: 'https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md')
+ it 'renders the version check badge' do
+ expect(rendered).to have_selector('.js-gitlab-version-check')
+ end
end
end
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index 43e11d31611..eaa909a5da0 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -115,4 +115,52 @@ RSpec.describe 'groups/edit.html.haml' do
end
end
end
+
+ context 'ip_restriction' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+
+ assign(:group, group)
+ allow(view).to receive(:current_user) { user }
+ end
+
+ context 'prompt user about registration features' do
+ before do
+ if Gitlab.ee?
+ allow(License).to receive(:current).and_return(nil)
+ end
+ end
+
+ context 'with service ping disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+ end
+
+ it 'renders a placeholder input with registration features message' do
+ render
+
+ expect(rendered).to have_field(:group_disabled_ip_restriction_ranges, disabled: true)
+ expect(rendered).to have_content(s_("RegistrationFeatures|Want to %{feature_title} for free?") % { feature_title: s_('RegistrationFeatures|use this feature') })
+ expect(rendered).to have_link(s_('RegistrationFeatures|Registration Features Program'))
+ end
+ end
+
+ context 'with service ping enabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ it 'does not render a placeholder input with registration features message' do
+ render
+
+ expect(rendered).not_to have_field(:group_disabled_ip_restriction_ranges, disabled: true)
+ expect(rendered).not_to have_content(s_("RegistrationFeatures|Want to %{feature_title} for free?") % { feature_title: s_('RegistrationFeatures|use this feature') })
+ expect(rendered).not_to have_link(s_('RegistrationFeatures|Registration Features Program'))
+ end
+ end
+ end
+ end
end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 600e431b7ef..1d26afcc567 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -76,7 +76,6 @@ RSpec.describe 'help/index' do
def stub_helpers
allow(view).to receive(:markdown).and_return('')
- allow(view).to receive(:version_status_badge).and_return('')
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
end
diff --git a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
new file mode 100644
index 00000000000..0e24810f835
--- /dev/null
+++ b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/header/_gitlab_version' do
+ describe 'when show_version_check? is true' do
+ before do
+ allow(view).to receive(:show_version_check?).and_return(true)
+ render
+ end
+
+ it 'renders the version check badge' do
+ expect(rendered).to have_selector('.js-gitlab-version-check')
+ end
+ end
+end
diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb
index d5a605958dc..624d7492aea 100644
--- a/spec/views/profiles/keys/_form.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_form.html.haml_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'profiles/keys/_form.html.haml' do
+ include SshKeysHelper
+
let_it_be(:key) { Key.new }
let(:page) { Capybara::Node::Simple.new(rendered) }
@@ -23,8 +25,8 @@ RSpec.describe 'profiles/keys/_form.html.haml' do
end
it 'has the key field', :aggregate_failures do
- expect(rendered).to have_field('Key', type: 'textarea', placeholder: 'Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
- expect(rendered).to have_text("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity.")
+ expect(rendered).to have_field('Key', type: 'textarea')
+ expect(rendered).to have_text(s_('Profiles|Begins with %{ssh_key_algorithms}.') % { ssh_key_algorithms: ssh_key_allowed_algorithms })
end
it 'has the title field', :aggregate_failures do
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index ed93240abc1..5c66fbe7dd7 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
allow(commit).to receive(:different_committer?).and_return(true)
allow(commit).to receive(:committer).and_return(committer)
- render partial: template, locals: {
+ render partial: template, formats: :html, locals: {
project: project,
ref: ref,
commit: commit
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 8c96f286c79..11f542767f4 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -29,19 +29,6 @@ RSpec.describe 'projects/edit' do
end
context 'merge suggestions settings' do
- it 'displays all possible variables' do
- render
-
- expect(rendered).to have_content('%{branch_name}')
- expect(rendered).to have_content('%{files_count}')
- expect(rendered).to have_content('%{file_paths}')
- expect(rendered).to have_content('%{project_name}')
- expect(rendered).to have_content('%{project_path}')
- expect(rendered).to have_content('%{user_full_name}')
- expect(rendered).to have_content('%{username}')
- expect(rendered).to have_content('%{suggestions_count}')
- end
-
it 'displays a placeholder if none is set' do
render
@@ -58,17 +45,6 @@ RSpec.describe 'projects/edit' do
end
context 'merge commit template' do
- it 'displays all possible variables' do
- render
-
- expect(rendered).to have_content('%{source_branch}')
- expect(rendered).to have_content('%{target_branch}')
- expect(rendered).to have_content('%{title}')
- expect(rendered).to have_content('%{issues}')
- expect(rendered).to have_content('%{description}')
- expect(rendered).to have_content('%{reference}')
- end
-
it 'displays a placeholder if none is set' do
render
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 6b6bc1f0b14..6ffd0936003 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -45,32 +45,4 @@ RSpec.describe 'projects/merge_requests/show.html.haml', :aggregate_failures do
end
end
end
-
- describe 'gitpod modal' do
- let(:gitpod_modal_selector) { '#modal-enable-gitpod' }
- let(:user) { create(:user) }
- let(:user_gitpod_enabled) { create(:user).tap { |x| x.update!(gitpod_enabled: true) } }
-
- where(:site_enabled, :current_user, :should_show) do
- false | ref(:user) | false
- true | ref(:user) | true
- true | nil | true
- true | ref(:user_gitpod_enabled) | false
- end
-
- with_them do
- it 'handles rendering gitpod user enable modal' do
- allow(Gitlab::CurrentSettings).to receive(:gitpod_enabled).and_return(site_enabled)
- allow(view).to receive(:current_user).and_return(current_user)
-
- render
-
- if should_show
- expect(rendered).to have_css(gitpod_modal_selector)
- else
- expect(rendered).to have_no_css(gitpod_modal_selector)
- end
- end
- end
- end
end
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
index 177f703ba6c..f212fd78b1a 100644
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -20,13 +20,33 @@ RSpec.describe 'projects/services/_form' do
)
end
- context 'commit_events and merge_request_events' do
- it 'display merge_request_events and commit_events descriptions' do
- allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
-
+ context 'integrations form' do
+ it 'does not render form element' do
render
- expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
+ expect(rendered).not_to have_selector('[data-testid="integration-form"]')
+ end
+
+ context 'when vue_integration_form feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_integration_form: false)
+ end
+
+ it 'renders form element' do
+ render
+
+ expect(rendered).to have_selector('[data-testid="integration-form"]')
+ end
+
+ context 'commit_events and merge_request_events' do
+ it 'display merge_request_events and commit_events descriptions' do
+ allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
+
+ render
+
+ expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
+ end
+ end
end
end
end
diff --git a/spec/views/shared/access_tokens/_table.html.haml_spec.rb b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
index 0a23768b4f1..fca2fc3183c 100644
--- a/spec/views/shared/access_tokens/_table.html.haml_spec.rb
+++ b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
let_it_be(:user) { create(:user) }
let_it_be(:tokens) { [create(:personal_access_token, user: user)] }
- let_it_be(:project) { false }
+ let_it_be(:resource) { false }
before do
stub_licensed_features(enforce_personal_access_token_expiration: true)
@@ -20,8 +20,8 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
allow(view).to receive(:personal_access_token_expiration_enforced?).and_return(token_expiry_enforced?)
allow(view).to receive(:show_profile_token_expiry_notification?).and_return(true)
- if project
- project.add_maintainer(user)
+ if resource
+ resource.add_maintainer(user)
end
# Forcibly removing scopes from one token as it's not possible to do with the current modal on creation
@@ -34,7 +34,7 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
type: type,
type_plural: type_plural,
active_tokens: tokens,
- project: project,
+ resource: resource,
impersonation: impersonation,
revoke_route_helper: ->(token) { 'path/' }
}
@@ -80,8 +80,8 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
end
end
- context 'if project' do
- let_it_be(:project) { create(:project) }
+ context 'if resource is project' do
+ let_it_be(:resource) { create(:project) }
it 'shows the project content', :aggregate_failures do
expect(rendered).to have_selector 'th', text: 'Role'
@@ -92,6 +92,18 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do
end
end
+ context 'if resource is group' do
+ let_it_be(:resource) { create(:group) }
+
+ it 'shows the group content', :aggregate_failures do
+ expect(rendered).to have_selector 'th', text: 'Role'
+ expect(rendered).to have_selector 'td', text: 'Maintainer'
+
+ expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
+ expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
+ end
+ end
+
context 'without tokens' do
let_it_be(:tokens) { [] }
diff --git a/spec/views/shared/nav/_sidebar.html.haml_spec.rb b/spec/views/shared/nav/_sidebar.html.haml_spec.rb
index 2eeebdff7a8..0eb945f5624 100644
--- a/spec/views/shared/nav/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/nav/_sidebar.html.haml_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe 'shared/nav/_sidebar.html.haml' do
- let(:project) { build(:project, id: non_existing_record_id) }
+ let_it_be(:project) { create(:project) }
+
let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) }
let(:sidebar) { Sidebars::Projects::Panel.new(context) }
diff --git a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
index 70991369506..bf050d601e3 100644
--- a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe 'shared/wikis/_sidebar.html.haml' do
let_it_be(:project) { create(:project) }
- let_it_be(:wiki) { Wiki.for_container(project, project.default_owner) }
+ let_it_be(:wiki) { Wiki.for_container(project, project.first_owner) }
before do
assign(:wiki, wiki)
diff --git a/spec/workers/ci/build_finished_worker_spec.rb b/spec/workers/ci/build_finished_worker_spec.rb
index 9096b0d2ba9..839723ac2fc 100644
--- a/spec/workers/ci/build_finished_worker_spec.rb
+++ b/spec/workers/ci/build_finished_worker_spec.rb
@@ -50,6 +50,21 @@ RSpec.describe Ci::BuildFinishedWorker do
subject
end
+
+ context 'when a build can be auto-retried' do
+ before do
+ allow(build)
+ .to receive(:auto_retry_allowed?)
+ .and_return(true)
+ end
+
+ it 'does not add a todo' do
+ expect(::Ci::MergeRequests::AddTodoWhenBuildFailsWorker)
+ .not_to receive(:perform_async)
+
+ subject
+ end
+ end
end
context 'when build has a chat' do
diff --git a/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb b/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb
new file mode 100644
index 00000000000..0460738f3f2
--- /dev/null
+++ b/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker do
+ let(:worker) { described_class.new }
+ let(:current_time) { Time.current }
+
+ let_it_be(:project) { create(:project) }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ describe '#perform' do
+ it 'executes ExpireProjectArtifactsService service with the project' do
+ expect_next_instance_of(Ci::JobArtifacts::ExpireProjectBuildArtifactsService, project.id, current_time) do |instance|
+ expect(instance).to receive(:execute).and_call_original
+ end
+
+ worker.perform(project.id)
+ end
+
+ context 'when project does not exist' do
+ it 'does nothing' do
+ expect(Ci::JobArtifacts::ExpireProjectBuildArtifactsService).not_to receive(:new)
+
+ worker.perform(non_existing_record_id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb b/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb
new file mode 100644
index 00000000000..1a5ca744091
--- /dev/null
+++ b/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::DeleteExpiredEventsWorker do
+ let(:agent) { create(:cluster_agent) }
+
+ describe '#perform' do
+ let(:agent_id) { agent.id }
+ let(:deletion_service) { double(execute: true) }
+
+ subject { described_class.new.perform(agent_id) }
+
+ it 'calls the deletion service' do
+ expect(deletion_service).to receive(:execute).once
+ expect(Clusters::Agents::DeleteExpiredEventsService).to receive(:new)
+ .with(agent).and_return(deletion_service)
+
+ subject
+ end
+
+ context 'agent no longer exists' do
+ let(:agent_id) { -1 }
+
+ it 'completes without raising an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 7608b5f49a1..85731de2a45 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -287,12 +287,6 @@ RSpec.describe ApplicationWorker do
end
context 'different kinds of push_bulk' do
- shared_context 'disable the `sidekiq_push_bulk_in_batches` feature flag' do
- before do
- stub_feature_flags(sidekiq_push_bulk_in_batches: false)
- end
- end
-
shared_context 'set safe limit beyond the number of jobs to be enqueued' do
before do
stub_const("#{described_class}::SAFE_PUSH_BULK_LIMIT", args.count + 1)
@@ -408,27 +402,6 @@ RSpec.describe ApplicationWorker do
it_behaves_like 'returns job_id of all enqueued jobs'
it_behaves_like 'does not schedule the jobs for any specific time'
end
-
- context 'when the feature flag `sidekiq_push_bulk_in_batches` is disabled' do
- include_context 'disable the `sidekiq_push_bulk_in_batches` feature flag'
-
- context 'when the number of jobs to be enqueued does not exceed the safe limit' do
- include_context 'set safe limit beyond the number of jobs to be enqueued'
-
- it_behaves_like 'enqueues jobs in one go'
- it_behaves_like 'logs bulk insertions'
- it_behaves_like 'returns job_id of all enqueued jobs'
- it_behaves_like 'does not schedule the jobs for any specific time'
- end
-
- context 'when the number of jobs to be enqueued exceeds safe limit' do
- include_context 'set safe limit below the number of jobs to be enqueued'
-
- it_behaves_like 'enqueues jobs in one go'
- it_behaves_like 'returns job_id of all enqueued jobs'
- it_behaves_like 'does not schedule the jobs for any specific time'
- end
- end
end
end
@@ -476,26 +449,6 @@ RSpec.describe ApplicationWorker do
it_behaves_like 'returns job_id of all enqueued jobs'
it_behaves_like 'schedules all the jobs at a specific time'
end
-
- context 'when the feature flag `sidekiq_push_bulk_in_batches` is disabled' do
- include_context 'disable the `sidekiq_push_bulk_in_batches` feature flag'
-
- context 'when the number of jobs to be enqueued does not exceed the safe limit' do
- include_context 'set safe limit beyond the number of jobs to be enqueued'
-
- it_behaves_like 'enqueues jobs in one go'
- it_behaves_like 'returns job_id of all enqueued jobs'
- it_behaves_like 'schedules all the jobs at a specific time'
- end
-
- context 'when the number of jobs to be enqueued exceeds safe limit' do
- include_context 'set safe limit below the number of jobs to be enqueued'
-
- it_behaves_like 'enqueues jobs in one go'
- it_behaves_like 'returns job_id of all enqueued jobs'
- it_behaves_like 'schedules all the jobs at a specific time'
- end
- end
end
end
@@ -575,26 +528,6 @@ RSpec.describe ApplicationWorker do
it_behaves_like 'returns job_id of all enqueued jobs'
it_behaves_like 'schedules all the jobs at a specific time, per batch'
end
-
- context 'when the feature flag `sidekiq_push_bulk_in_batches` is disabled' do
- include_context 'disable the `sidekiq_push_bulk_in_batches` feature flag'
-
- context 'when the number of jobs to be enqueued does not exceed the safe limit' do
- include_context 'set safe limit beyond the number of jobs to be enqueued'
-
- it_behaves_like 'enqueues jobs in one go'
- it_behaves_like 'returns job_id of all enqueued jobs'
- it_behaves_like 'schedules all the jobs at a specific time, per batch'
- end
-
- context 'when the number of jobs to be enqueued exceeds safe limit' do
- include_context 'set safe limit below the number of jobs to be enqueued'
-
- it_behaves_like 'enqueues jobs in one go'
- it_behaves_like 'returns job_id of all enqueued jobs'
- it_behaves_like 'schedules all the jobs at a specific time, per batch'
- end
- end
end
end
end
diff --git a/spec/workers/concerns/cluster_agent_queue_spec.rb b/spec/workers/concerns/cluster_agent_queue_spec.rb
new file mode 100644
index 00000000000..b5189cbd8c8
--- /dev/null
+++ b/spec/workers/concerns/cluster_agent_queue_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClusterAgentQueue do
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'ExampleWorker'
+ end
+
+ include ApplicationWorker
+ include ClusterAgentQueue
+ end
+ end
+
+ it { expect(worker.queue).to eq('cluster_agent:example') }
+ it { expect(worker.get_feature_category).to eq(:kubernetes_management) }
+end
diff --git a/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb b/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb
new file mode 100644
index 00000000000..95962d4810e
--- /dev/null
+++ b/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Packages::CleanupArtifactWorker do
+ let_it_be(:worker_class) do
+ Class.new do
+ def self.name
+ 'Gitlab::Foo::Bar::DummyWorker'
+ end
+
+ include ApplicationWorker
+ include ::Packages::CleanupArtifactWorker
+ end
+ end
+
+ let(:worker) { worker_class.new }
+
+ describe '#model' do
+ subject { worker.send(:model) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#log_metadata' do
+ subject { worker.send(:log_metadata) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#log_cleanup_item' do
+ subject { worker.send(:log_cleanup_item) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+end
diff --git a/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb b/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb
index ed0bdefbdb8..1100f9a7fae 100644
--- a/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb
+++ b/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe DependencyProxy::CleanupDependencyProxyWorker do
context 'when there are records to be deleted' do
it_behaves_like 'an idempotent worker' do
it 'queues the cleanup jobs', :aggregate_failures do
- create(:dependency_proxy_blob, :expired)
- create(:dependency_proxy_manifest, :expired)
+ create(:dependency_proxy_blob, :pending_destruction)
+ create(:dependency_proxy_manifest, :pending_destruction)
expect(DependencyProxy::CleanupBlobWorker).to receive(:perform_with_capacity).twice
expect(DependencyProxy::CleanupManifestWorker).to receive(:perform_with_capacity).twice
diff --git a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
index b035a2ec0b7..6a2fdfbe8f5 100644
--- a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
+++ b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
@@ -17,19 +17,19 @@ RSpec.describe DependencyProxy::ImageTtlGroupPolicyWorker do
let_it_be_with_reload(:new_blob) { create(:dependency_proxy_blob, group: group) }
let_it_be_with_reload(:new_manifest) { create(:dependency_proxy_manifest, group: group) }
- it 'updates the old images to expired' do
+ it 'updates the old images to pending_destruction' do
expect { subject }
- .to change { old_blob.reload.status }.from('default').to('expired')
- .and change { old_manifest.reload.status }.from('default').to('expired')
+ .to change { old_blob.reload.status }.from('default').to('pending_destruction')
+ .and change { old_manifest.reload.status }.from('default').to('pending_destruction')
.and not_change { new_blob.reload.status }
.and not_change { new_manifest.reload.status }
end
end
context 'counts logging' do
- let_it_be(:expired_blob) { create(:dependency_proxy_blob, :expired, group: group) }
- let_it_be(:expired_blob2) { create(:dependency_proxy_blob, :expired, group: group) }
- let_it_be(:expired_manifest) { create(:dependency_proxy_manifest, :expired, group: group) }
+ let_it_be(:expired_blob) { create(:dependency_proxy_blob, :pending_destruction, group: group) }
+ let_it_be(:expired_blob2) { create(:dependency_proxy_blob, :pending_destruction, group: group) }
+ let_it_be(:expired_manifest) { create(:dependency_proxy_manifest, :pending_destruction, group: group) }
let_it_be(:processing_blob) { create(:dependency_proxy_blob, status: :processing, group: group) }
let_it_be(:processing_manifest) { create(:dependency_proxy_manifest, status: :processing, group: group) }
let_it_be(:error_blob) { create(:dependency_proxy_blob, status: :error, group: group) }
diff --git a/spec/workers/deployments/hooks_worker_spec.rb b/spec/workers/deployments/hooks_worker_spec.rb
index b4a91cff2ac..50ead66cfbf 100644
--- a/spec/workers/deployments/hooks_worker_spec.rb
+++ b/spec/workers/deployments/hooks_worker_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Deployments::HooksWorker do
it 'executes project services for deployment_hooks' do
deployment = create(:deployment, :running)
project = deployment.project
- service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
+ service = create(:integration, type: 'SlackService', project: project, deployment_events: true, active: true)
expect(ProjectServiceWorker).to receive(:perform_async).with(service.id, an_instance_of(Hash))
@@ -23,7 +23,7 @@ RSpec.describe Deployments::HooksWorker do
it 'does not execute an inactive service' do
deployment = create(:deployment, :running)
project = deployment.project
- create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
+ create(:integration, type: 'SlackService', project: project, deployment_events: true, active: false)
expect(ProjectServiceWorker).not_to receive(:perform_async)
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index 83720ee132b..dba535654a1 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -21,87 +21,45 @@ RSpec.describe EmailReceiverWorker, :mailer do
context "when an error occurs" do
before do
allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(error)
- expect(Sidekiq.logger).to receive(:error).with(hash_including('exception.class' => error.class.name)).and_call_original
end
- context 'when the error is Gitlab::Email::EmptyEmailError' do
+ context 'when error is a processing error' do
let(:error) { Gitlab::Email::EmptyEmailError.new }
- it 'sends out a rejection email' do
- perform_enqueued_jobs do
- described_class.new.perform(raw_message)
+ it 'triggers email failure handler' do
+ expect(Gitlab::Email::FailureHandler).to receive(:handle) do |receiver, received_error|
+ expect(receiver).to be_a(Gitlab::Email::Receiver)
+ expect(receiver.mail.encoded).to eql(Mail::Message.new(raw_message).encoded)
+ expect(received_error).to be(error)
end
- email = ActionMailer::Base.deliveries.last
- expect(email).not_to be_nil
- expect(email.to).to eq(["jake@adventuretime.ooo"])
- expect(email.subject).to include("Rejected")
- end
-
- it 'strips out the body before passing to EmailRejectionMailer' do
- mail = Mail.new(raw_message)
- mail.body = nil
-
- expect(EmailRejectionMailer).to receive(:rejection).with(anything, mail.encoded, anything).and_call_original
-
described_class.new.perform(raw_message)
end
- end
-
- context 'when the error is Gitlab::Email::AutoGeneratedEmailError' do
- let(:error) { Gitlab::Email::AutoGeneratedEmailError.new }
-
- it 'does not send out any rejection email' do
- perform_enqueued_jobs do
- described_class.new.perform(raw_message)
- end
-
- should_not_email_anyone
- end
- end
- context 'when the error is Gitlab::Email::InvalidAttachment' do
- let(:error) { Gitlab::Email::InvalidAttachment.new("Could not deal with that") }
+ it 'logs the error' do
+ expect(Sidekiq.logger).to receive(:error).with(hash_including('exception.class' => error.class.name)).and_call_original
- it 'reports the error to the sender' do
- perform_enqueued_jobs do
- described_class.new.perform(raw_message)
- end
-
- email = ActionMailer::Base.deliveries.last
- expect(email).not_to be_nil
- expect(email.to).to eq(["jake@adventuretime.ooo"])
- expect(email.body.parts.last.to_s).to include("Could not deal with that")
+ described_class.new.perform(raw_message)
end
end
- context 'when the error is ActiveRecord::StatementTimeout' do
+ context 'when error is not a processing error' do
let(:error) { ActiveRecord::StatementTimeout.new("Statement timeout") }
- it 'does not report the error to the sender' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error).and_call_original
-
- perform_enqueued_jobs do
- described_class.new.perform(raw_message)
+ it 'triggers email failure handler' do
+ expect(Gitlab::Email::FailureHandler).to receive(:handle) do |receiver, received_error|
+ expect(receiver).to be_a(Gitlab::Email::Receiver)
+ expect(receiver.mail.encoded).to eql(Mail::Message.new(raw_message).encoded)
+ expect(received_error).to be(error)
end
- email = ActionMailer::Base.deliveries.last
- expect(email).to be_nil
+ described_class.new.perform(raw_message)
end
- end
-
- context 'when the error is RateLimitedService::RateLimitedError' do
- let(:error) { RateLimitedService::RateLimitedError.new(key: :issues_create, rate_limiter: Gitlab::ApplicationRateLimiter) }
- it 'does not report the error to the sender' do
+ it 'reports the error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error).and_call_original
- perform_enqueued_jobs do
- described_class.new.perform(raw_message)
- end
-
- email = ActionMailer::Base.deliveries.last
- expect(email).to be_nil
+ described_class.new.perform(raw_message)
end
end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 00b6d2635a5..bb4e2981070 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -361,6 +361,7 @@ RSpec.describe 'Every Sidekiq worker' do
'ObjectPool::ScheduleJoinWorker' => 3,
'ObjectStorage::BackgroundMoveWorker' => 5,
'ObjectStorage::MigrateUploadsWorker' => 3,
+ 'Packages::CleanupPackageFileWorker' => 0,
'Packages::Composer::CacheUpdateWorker' => false,
'Packages::Go::SyncPackagesWorker' => 3,
'Packages::Maven::Metadata::SyncWorker' => 3,
@@ -369,7 +370,7 @@ RSpec.describe 'Every Sidekiq worker' do
'PagesDomainSslRenewalWorker' => 3,
'PagesDomainVerificationWorker' => 3,
'PagesTransferWorker' => 3,
- 'PagesUpdateConfigurationWorker' => 3,
+ 'PagesUpdateConfigurationWorker' => 1,
'PagesWorker' => 3,
'PersonalAccessTokens::Groups::PolicyWorker' => 3,
'PersonalAccessTokens::Instance::PolicyWorker' => 3,
diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
index 3c628d036ff..497f95cf34d 100644
--- a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
+++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe LooseForeignKeys::CleanupWorker do
include MigrationsHelpers
+ using RSpec::Parameterized::TableSyntax
def create_table_structure
migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)
@@ -149,4 +150,31 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
expect { described_class.new.perform }.not_to change { LooseForeignKeys::DeletedRecord.status_processed.count }
end
end
+
+ describe 'multi-database support' do
+ where(:current_minute, :configured_base_models, :expected_connection) do
+ 2 | { main: ApplicationRecord, ci: Ci::ApplicationRecord } | ApplicationRecord.connection
+ 3 | { main: ApplicationRecord, ci: Ci::ApplicationRecord } | Ci::ApplicationRecord.connection
+ 2 | { main: ApplicationRecord } | ApplicationRecord.connection
+ 3 | { main: ApplicationRecord } | ApplicationRecord.connection
+ end
+
+ with_them do
+ before do
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(configured_base_models)
+ end
+
+ it 'uses the correct connection' do
+ LooseForeignKeys::DeletedRecord.count.times do
+ expect_next_found_instance_of(LooseForeignKeys::DeletedRecord) do |instance|
+ expect(instance.class.connection).to eq(expected_connection)
+ end
+ end
+
+ travel_to DateTime.new(2019, 1, 1, 10, current_minute) do
+ described_class.new.perform
+ end
+ end
+ end
+ end
end
diff --git a/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb b/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..f3ea14ad539
--- /dev/null
+++ b/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::UpdateHeadPipelineWorker do
+ include ProjectForksHelper
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:ref) { 'master' }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: ref) }
+ let(:event) { Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id }) }
+
+ subject { consume_event(event) }
+
+ def consume_event(event)
+ described_class.new.perform(event.class.name, event.data)
+ end
+
+ context 'when merge requests already exist for this source branch', :sidekiq_inline do
+ let(:merge_request_1) do
+ create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project)
+ end
+
+ let(:merge_request_2) do
+ create(:merge_request, source_branch: 'feature', target_branch: "v1.1.0", source_project: project)
+ end
+
+ context 'when related merge request is already merged' do
+ let!(:merged_merge_request) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged')
+ end
+
+ it 'does not schedule update head pipeline job' do
+ expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id)
+
+ subject
+ end
+ end
+
+ context 'when the head pipeline sha equals merge request sha' do
+ let(:ref) { 'feature' }
+
+ before do
+ pipeline.update!(sha: project.repository.commit(ref).id)
+ end
+
+ it 'updates head pipeline of each merge request' do
+ merge_request_1
+ merge_request_2
+
+ subject
+
+ expect(merge_request_1.reload.head_pipeline).to eq(pipeline)
+ expect(merge_request_2.reload.head_pipeline).to eq(pipeline)
+ end
+ end
+
+ context 'when the head pipeline sha does not equal merge request sha' do
+ let(:ref) { 'feature' }
+
+ it 'does not update the head piepeline of MRs' do
+ merge_request_1
+ merge_request_2
+
+ subject
+
+ expect(merge_request_1.reload.head_pipeline).not_to eq(pipeline)
+ expect(merge_request_2.reload.head_pipeline).not_to eq(pipeline)
+ end
+ end
+
+ context 'when there is no pipeline for source branch' do
+ it "does not update merge request head pipeline" do
+ merge_request = create(:merge_request, source_branch: 'feature',
+ target_branch: "branch_1",
+ source_project: project)
+
+ subject
+
+ expect(merge_request.reload.head_pipeline).not_to eq(pipeline)
+ end
+ end
+
+ context 'when merge request target project is different from source project' do
+ let(:project) { fork_project(target_project, nil, repository: true) }
+ let(:target_project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:ref) { 'feature' }
+
+ before do
+ project.add_developer(user)
+ pipeline.update!(sha: project.repository.commit(ref).id)
+ end
+
+ it 'updates head pipeline for merge request' do
+ merge_request = create(:merge_request, source_branch: 'feature',
+ target_branch: "master",
+ source_project: project,
+ target_project: target_project)
+
+ subject
+
+ expect(merge_request.reload.head_pipeline).to eq(pipeline)
+ end
+ end
+
+ context 'when the pipeline is not the latest for the branch' do
+ it 'does not update merge request head pipeline' do
+ merge_request = create(:merge_request, source_branch: 'master',
+ target_branch: "branch_1",
+ source_project: project)
+
+ create(:ci_pipeline, project: pipeline.project, ref: pipeline.ref)
+
+ subject
+
+ expect(merge_request.reload.head_pipeline).to be_nil
+ end
+ end
+
+ context 'when pipeline has errors' do
+ before do
+ pipeline.update!(yaml_errors: 'some errors', status: :failed)
+ end
+
+ it 'updates merge request head pipeline reference' do
+ merge_request = create(:merge_request, source_branch: 'master',
+ target_branch: 'feature',
+ source_project: project)
+
+ subject
+
+ expect(merge_request.reload.head_pipeline).to eq(pipeline)
+ end
+ end
+ end
+end
diff --git a/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb b/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
index 19b79835825..f151780ffd7 100644
--- a/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
+++ b/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
@@ -10,16 +10,34 @@ RSpec.describe Metrics::Dashboard::SyncDashboardsWorker do
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
describe ".perform" do
- it 'imports metrics' do
- expect { worker.perform(project.id) }.to change(PrometheusMetric, :count).by(3)
+ context 'with valid dashboard hash' do
+ it 'imports metrics' do
+ expect { worker.perform(project.id) }.to change(PrometheusMetric, :count).by(3)
+ end
+
+ it 'is idempotent' do
+ 2.times do
+ worker.perform(project.id)
+ end
+
+ expect(PrometheusMetric.count).to eq(3)
+ end
end
- it 'is idempotent' do
- 2.times do
- worker.perform(project.id)
+ context 'with invalid dashboard hash' do
+ before do
+ allow_next_instance_of(Gitlab::Metrics::Dashboard::Importer) do |instance|
+ allow(instance).to receive(:dashboard_hash).and_return({})
+ end
end
- expect(PrometheusMetric.count).to eq(3)
+ it 'does not import metrics' do
+ expect { worker.perform(project.id) }.not_to change(PrometheusMetric, :count)
+ end
+
+ it 'does not raise an error' do
+ expect { worker.perform(project.id) }.not_to raise_error
+ end
end
end
end
diff --git a/spec/workers/packages/cleanup_package_file_worker_spec.rb b/spec/workers/packages/cleanup_package_file_worker_spec.rb
new file mode 100644
index 00000000000..b423c4d3f06
--- /dev/null
+++ b/spec/workers/packages/cleanup_package_file_worker_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::CleanupPackageFileWorker do
+ let_it_be(:package) { create(:package) }
+
+ let(:worker) { described_class.new }
+
+ describe '#perform_work' do
+ subject { worker.perform_work }
+
+ context 'with no work to do' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with work to do' do
+ let_it_be(:package_file1) { create(:package_file, package: package) }
+ let_it_be(:package_file2) { create(:package_file, :pending_destruction, package: package) }
+ let_it_be(:package_file3) { create(:package_file, :pending_destruction, package: package, updated_at: 1.year.ago, created_at: 1.year.ago) }
+
+ it 'deletes the oldest package file pending destruction based on id', :aggregate_failures do
+ expect(worker).to receive(:log_extra_metadata_on_done).twice
+
+ expect { subject }.to change { Packages::PackageFile.count }.by(-1)
+ end
+ end
+
+ context 'with an error during the destroy' do
+ let_it_be(:package_file) { create(:package_file, :pending_destruction) }
+
+ before do
+ expect(worker).to receive(:log_metadata).and_raise('Error!')
+ end
+
+ it 'handles the error' do
+ expect { subject }.to change { Packages::PackageFile.error.count }.from(0).to(1)
+ expect(package_file.reload).to be_error
+ end
+ end
+ end
+
+ describe '#max_running_jobs' do
+ let(:capacity) { 5 }
+
+ subject { worker.max_running_jobs }
+
+ before do
+ stub_application_setting(packages_cleanup_package_file_worker_capacity: capacity)
+ end
+
+ it { is_expected.to eq(capacity) }
+ end
+
+ describe '#remaining_work_count' do
+ before(:context) do
+ create_list(:package_file, 3, :pending_destruction, package: package)
+ end
+
+ subject { worker.remaining_work_count }
+
+ it { is_expected.to eq(3) }
+ end
+end
diff --git a/spec/workers/packages/cleanup_package_registry_worker_spec.rb b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
new file mode 100644
index 00000000000..e43864975f6
--- /dev/null
+++ b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::CleanupPackageRegistryWorker do
+ describe '#perform' do
+ let_it_be_with_reload(:package_files) { create_list(:package_file, 2, :pending_destruction) }
+
+ let(:worker) { described_class.new }
+
+ subject(:perform) { worker.perform }
+
+ context 'with package files pending destruction' do
+ it_behaves_like 'an idempotent worker'
+
+ it 'queues the cleanup job' do
+ expect(Packages::CleanupPackageFileWorker).to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
+ context 'with no package files pending destruction' do
+ before do
+ ::Packages::PackageFile.update_all(status: :default)
+ end
+
+ it_behaves_like 'an idempotent worker'
+
+ it 'does not queue the cleanup job' do
+ expect(Packages::CleanupPackageFileWorker).not_to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
+ describe 'counts logging' do
+ let_it_be(:processing_package_file) { create(:package_file, status: :processing) }
+
+ it 'logs all the counts', :aggregate_failures do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:pending_destruction_package_files_count, 2)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:processing_package_files_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:error_package_files_count, 0)
+
+ perform
+ end
+
+ context 'with load balancing enabled', :db_load_balancing do
+ it 'reads the count from the replica' do
+ expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original
+
+ perform
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/pages_update_configuration_worker_spec.rb b/spec/workers/pages_update_configuration_worker_spec.rb
index 7cceeaa52d6..af71f6b3cca 100644
--- a/spec/workers/pages_update_configuration_worker_spec.rb
+++ b/spec/workers/pages_update_configuration_worker_spec.rb
@@ -5,59 +5,8 @@ RSpec.describe PagesUpdateConfigurationWorker do
let_it_be(:project) { create(:project) }
describe "#perform" do
- it "does not break if the project doesn't exist" do
+ it "does not break" do
expect { subject.perform(-1) }.not_to raise_error
end
-
- it "calls the correct service" do
- expect_next_instance_of(Projects::UpdatePagesConfigurationService, project) do |service|
- expect(service).to receive(:execute).and_return({})
- end
-
- subject.perform(project.id)
- end
-
- it_behaves_like "an idempotent worker" do
- let(:job_args) { [project.id] }
- let(:pages_dir) { Dir.mktmpdir }
- let(:config_path) { File.join(pages_dir, "config.json") }
-
- before do
- allow(Project).to receive(:find_by_id).with(project.id).and_return(project)
- allow(project).to receive(:pages_path).and_return(pages_dir)
-
- # Make sure _some_ config exists
- FileUtils.touch(config_path)
- end
-
- after do
- FileUtils.remove_entry(pages_dir)
- end
-
- it "only updates the config file once" do
- described_class.new.perform(project.id)
-
- expect(File.mtime(config_path)).not_to be_nil
- expect { subject }.not_to change { File.mtime(config_path) }
- end
- end
- end
-
- describe '#perform_async' do
- it "calls the correct service", :sidekiq_inline do
- expect_next_instance_of(Projects::UpdatePagesConfigurationService, project) do |service|
- expect(service).to receive(:execute).and_return(status: :success)
- end
-
- described_class.perform_async(project.id)
- end
-
- it "doesn't schedule a worker if updates on legacy storage are disabled", :sidekiq_inline do
- allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
-
- expect(Projects::UpdatePagesConfigurationService).not_to receive(:new)
-
- described_class.perform_async(project.id)
- end
end
end
diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb
new file mode 100644
index 00000000000..5ddfd5b43b9
--- /dev/null
+++ b/spec/workers/pages_worker_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PagesWorker, :sidekiq_inline do
+ let(:project) { create(:project) }
+ let(:ci_build) { create(:ci_build, project: project)}
+
+ it 'calls UpdatePagesService' do
+ expect_next_instance_of(Projects::UpdatePagesService, project, ci_build) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ described_class.perform_async(:deploy, ci_build.id)
+ end
+end
diff --git a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
index b928104fb58..3de59670f8d 100644
--- a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
+++ b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
@@ -25,11 +25,11 @@ RSpec.describe PurgeDependencyProxyCacheWorker do
include_examples 'an idempotent worker' do
let(:job_args) { [user.id, group_id] }
- it 'expires the blobs and returns ok', :aggregate_failures do
+ it 'marks the blobs as pending_destruction and returns ok', :aggregate_failures do
subject
- expect(blob).to be_expired
- expect(manifest).to be_expired
+ expect(blob).to be_pending_destruction
+ expect(manifest).to be_pending_destruction
end
end
end
diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb
index 0f40177eb7d..bbb8844a447 100644
--- a/spec/workers/web_hook_worker_spec.rb
+++ b/spec/workers/web_hook_worker_spec.rb
@@ -19,6 +19,15 @@ RSpec.describe WebHookWorker do
expect { subject.perform(non_existing_record_id, data, hook_name) }.not_to raise_error
end
+ it 'retrieves recursion detection data, reinstates it, and cleans it from payload', :request_store, :aggregate_failures do
+ uuid = SecureRandom.uuid
+ full_data = data.merge({ _gitlab_recursion_detection_request_uuid: uuid })
+
+ expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything).to receive(:execute)
+ expect { subject.perform(project_hook.id, full_data, hook_name) }
+ .to change { Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid }.to(uuid)
+ end
+
it_behaves_like 'worker with data consistency',
described_class,
data_consistency: :delayed